初始化前端目录
This commit is contained in:
3329
capi-ui/package-lock.json
generated
3329
capi-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,16 +9,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@ant-design/plots": "^2.6.3",
|
||||
"@iconify-icons/mdi": "^1.2.48",
|
||||
"@iconify/iconify": "^3.1.1",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.11.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"echarts": "^6.0.0",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.2.8",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/carbon": "^1.2.13",
|
||||
"@vitejs/plugin-vue": "^1.6.0",
|
||||
"@vue/compiler-sfc": "^3.2.6",
|
||||
"sass": "^1.91.0",
|
||||
|
||||
@@ -11,4 +11,7 @@ export interface LoginResult {
|
||||
|
||||
/** 登录接口 */
|
||||
export const apiLogin = (data: LoginParams) =>
|
||||
request.post<LoginResult>('/Sys/login/userLogin', data)
|
||||
request.post<LoginResult>('/Sys/login/userLogin', data)
|
||||
|
||||
/** 退出登录 */
|
||||
export const apiLogout = () => request.post('/Sys/login/userLogout')
|
||||
52
capi-ui/src/api/data.ts
Normal file
52
capi-ui/src/api/data.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export interface DbItem {
|
||||
label: string;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface dbResponse {
|
||||
code: number
|
||||
message: string
|
||||
result: DbItem[]
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface SyncTables {
|
||||
createTime: string;
|
||||
taskId: string;
|
||||
taskName: string;
|
||||
sourceDbId: string;
|
||||
sourceTable: string;
|
||||
targetTable: string;
|
||||
isActive: string;
|
||||
lastSyncTime: string;
|
||||
updateTime: string;
|
||||
dbType: string;
|
||||
dbId: string;
|
||||
successRows: number;
|
||||
}
|
||||
|
||||
|
||||
/** 获取数据库下拉列表 */
|
||||
export const getDbList = async (): Promise<DbItem[]> => {
|
||||
const response = await request.get<dbResponse>('/Sys/data/getDbList')
|
||||
|
||||
if (response.code !== 200) {
|
||||
console.error('获取数据失败:', response.message)
|
||||
throw new Error(`获取数据失败: ${response.message}`)
|
||||
}
|
||||
return response.result || []
|
||||
}
|
||||
|
||||
|
||||
export const getSyncTables = async (data: SyncTables): Promise<SyncTables[]> =>{
|
||||
const response = await request.get<SyncTables>('/Sys/data/getTableList', { params: data })
|
||||
if (response.code !== 200) {
|
||||
console.error('获取数据失败:', response.message)
|
||||
throw new Error(`获取数据失败: ${response.message}`)
|
||||
}
|
||||
return response.result || []
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<a-layout style="min-height: 100vh;">
|
||||
<!-- 顶部导航栏(无修改) -->
|
||||
<!-- 顶部导航栏 -->
|
||||
<a-layout-header class="header">
|
||||
<div class="logo">
|
||||
<img src="/my.png" class="logo-img" alt="系统Logo" />
|
||||
@@ -9,14 +9,23 @@
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="search-box">
|
||||
<!-- 直接使用全局注册的图标 -->
|
||||
<SearchOutlined class="search-icon" />
|
||||
<input type="text" placeholder="搜索" class="search-input" />
|
||||
</div>
|
||||
<div class="action-icons">
|
||||
<a-tooltip title="收藏"><StarOutlined class="action-icon" /></a-tooltip>
|
||||
<a-tooltip title="通知"><BellOutlined class="action-icon" /></a-tooltip>
|
||||
<a-tooltip title="帮助"><QuestionCircleOutlined class="action-icon" /></a-tooltip>
|
||||
<a-tooltip title="设置"><SettingOutlined class="action-icon" /></a-tooltip>
|
||||
<a-tooltip title="收藏">
|
||||
<StarOutlined class="action-icon" @click="showFavorite = true" />
|
||||
</a-tooltip>
|
||||
<a-tooltip title="通知">
|
||||
<BellOutlined class="action-icon" @click="showNotification = true" />
|
||||
</a-tooltip>
|
||||
<a-tooltip title="帮助">
|
||||
<QuestionCircleOutlined class="action-icon" @click="showHelp = true" />
|
||||
</a-tooltip>
|
||||
<a-tooltip title="设置">
|
||||
<SettingOutlined class="action-icon" @click="showSettings = true" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-dropdown
|
||||
placement="bottomRight"
|
||||
@@ -28,10 +37,17 @@
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu class="user-menu" @click="handleUserMenuClick">
|
||||
<a-menu-item key="profile">个人资料</a-menu-item>
|
||||
<a-menu-item key="settings">账户设置</a-menu-item>
|
||||
<a-menu-item key=""> <img src="https://picsum.photos/id/1005/200/200" alt="用户头像" class="avatar-img" />{{userName}}</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="profile">
|
||||
<UserOutlined class="mr-2" /> 个人资料
|
||||
</a-menu-item>
|
||||
<a-menu-item key="settings">
|
||||
<ToolFilled class="mr-2" /> 修改密码
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout" class="logout-menu-item">
|
||||
<!-- 直接使用全局注册的图标 -->
|
||||
<LogoutOutlined class="mr-2" /> 退出登录
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
@@ -40,9 +56,9 @@
|
||||
</div>
|
||||
</a-layout-header>
|
||||
|
||||
<!-- 中间主体布局(菜单逻辑优化) -->
|
||||
<!-- 中间主体布局 -->
|
||||
<a-layout style="flex: 1; height: calc(100vh - 64px);">
|
||||
<!-- 左侧菜单(状态与路由同步优化) -->
|
||||
<!-- 左侧菜单 -->
|
||||
<a-layout-sider
|
||||
class="side-menu-container"
|
||||
width="200"
|
||||
@@ -67,6 +83,7 @@
|
||||
@openChange="(visible) => handleCollapsedMenuVisible(visible, module.moduleCode)"
|
||||
>
|
||||
<div class="parent-menu-header collapsed-mode" @click.stop>
|
||||
<!-- 动态图标:通过封装的图标库获取 -->
|
||||
<component :is="getIconComponent(module.icon)" class="parent-menu-icon" />
|
||||
<a-tooltip :title="module.moduleName" placement="right">
|
||||
<span class="collapsed-tooltip-placeholder" />
|
||||
@@ -81,6 +98,7 @@
|
||||
v-for="menu in module.menus"
|
||||
:key="menu.menuCode"
|
||||
:selected="selectedKey === menu.menuCode"
|
||||
:disabled="!menu.chref"
|
||||
>
|
||||
<component :is="getIconComponent(menu.cicon)" class="sub-menu-icon" />
|
||||
<span>{{ menu.menuName }}</span>
|
||||
@@ -97,8 +115,9 @@
|
||||
>
|
||||
<component :is="getIconComponent(module.icon)" class="parent-menu-icon" />
|
||||
<span class="parent-menu-title">{{ module.moduleName }}</span>
|
||||
<!-- 动态图标:直接使用全局注册的图标(无需映射) -->
|
||||
<component
|
||||
:is="isSubMenuOpen(module.moduleCode) ? CaretUpOutlined : CaretDownOutlined"
|
||||
:is="isSubMenuOpen(module.moduleCode) ? 'CaretUpOutlined' : 'CaretDownOutlined'"
|
||||
class="parent-menu-caret"
|
||||
/>
|
||||
</div>
|
||||
@@ -110,8 +129,12 @@
|
||||
v-for="menu in module.menus"
|
||||
:key="menu.menuCode"
|
||||
class="sub-menu-item"
|
||||
:class="{ 'sub-menu-item-active': selectedKey === menu.menuCode }"
|
||||
:class="{
|
||||
'sub-menu-item-active': selectedKey === menu.menuCode,
|
||||
'sub-menu-item-disabled': !menu.chref
|
||||
}"
|
||||
@click="handleSubMenuItemClick(menu)"
|
||||
:style="{ cursor: menu.chref ? 'pointer' : 'not-allowed' }"
|
||||
>
|
||||
<component :is="getIconComponent(menu.cicon)" class="sub-menu-icon" />
|
||||
<span>{{ menu.menuName }}</span>
|
||||
@@ -121,18 +144,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-trigger" @click="collapsed = !collapsed">
|
||||
<component :is="collapsed ? MenuUnfoldOutlined : MenuFoldOutlined" />
|
||||
<!-- 直接使用全局注册的图标 -->
|
||||
<component :is="collapsed ? 'MenuUnfoldOutlined' : 'MenuFoldOutlined'" />
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<!-- 右侧内容区(核心修改:用RouterView渲染路由组件) -->
|
||||
<!-- 右侧内容区 -->
|
||||
<a-layout-content class="right-content">
|
||||
<!-- 标签页(与路由绑定) -->
|
||||
<!-- 标签页 -->
|
||||
<div class="tab-bar">
|
||||
<a-tag
|
||||
:class="['console-tab', $route.path === '/console' ? 'console-tab--active' : '']"
|
||||
:class="['console-tab', $route.path === '/index/console' ? 'console-tab--active' : '']"
|
||||
@click="switchToConsole"
|
||||
>
|
||||
<!-- 直接使用全局注册的图标 -->
|
||||
<HomeOutlined class="console-tab-icon" />
|
||||
<span>控制台</span>
|
||||
</a-tag>
|
||||
@@ -174,29 +199,39 @@
|
||||
:disabled="otherTabs.length === 0"
|
||||
ghost
|
||||
>
|
||||
<!-- 直接使用全局注册的图标 -->
|
||||
<CloseOutlined class="close-icon" /> 关闭所有
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 内容区核心:RouterView渲染当前路由组件 + 加载/错误状态 -->
|
||||
<!-- 内容区核心:Suspense + RouterView -->
|
||||
<a-layout-content class="main-content">
|
||||
<div class="home-content">
|
||||
<!-- 加载状态(路由切换时显示) -->
|
||||
<div v-if="contentLoading" class="content-loading">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
|
||||
<!-- 路由组件渲染容器 -->
|
||||
<RouterView
|
||||
v-else
|
||||
@error="handleRouterViewError"
|
||||
/>
|
||||
|
||||
<!-- 组件加载失败提示 -->
|
||||
<Suspense>
|
||||
<template #default>
|
||||
<RouterView @error="handleRouterViewError" />
|
||||
</template>
|
||||
<template #fallback>
|
||||
<div class="content-loading">
|
||||
<a-spin size="large" tip="页面加载中..." />
|
||||
</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
|
||||
<div v-if="componentError" class="component-error">
|
||||
<!-- 直接使用全局注册的图标 -->
|
||||
<ExclamationCircleOutlined class="error-icon" />
|
||||
<p>组件加载失败,请检查路由配置或组件路径</p>
|
||||
<p class="error-title">组件加载失败</p>
|
||||
<p class="error-desc">请检查路由配置或页面文件是否存在</p>
|
||||
<p class="error-path">失败路由:{{ errorPath }}</p>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="middle"
|
||||
class="retry-btn"
|
||||
@click="reloadCurrentRoute"
|
||||
>
|
||||
重新加载
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-layout-content>
|
||||
@@ -204,7 +239,7 @@
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
|
||||
<!-- 退出确认对话框(无修改) -->
|
||||
<!-- 退出确认对话框 -->
|
||||
<a-modal
|
||||
v-model:open="logoutVisible"
|
||||
:centered="true"
|
||||
@@ -227,143 +262,185 @@
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
|
||||
<!-- 放在根节点最下方即可 -->
|
||||
<Favorite v-if="showFavorite" :visible="showFavorite" @close="showFavorite = false" />
|
||||
<Notification v-if="showNotification" :visible="showNotification" @close="showNotification = false" />
|
||||
<Help v-if="showHelp" :visible="showHelp" @close="showHelp = false" />
|
||||
<Settings v-if="showSettings" :visible="showSettings" @close="showSettings = false" />
|
||||
|
||||
<!-- 个人资料 -->
|
||||
<UserProfile v-if="showUserProfile" :visible="showUserProfile" @close="showUserProfile = false" />
|
||||
<!-- 修改密码 -->
|
||||
<ChangePwd v-if="showChangePwd" :visible="showChangePwd" @close="showChangePwd = false" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, h, reactive, watch, onMounted } from 'vue';
|
||||
import { useRouter, useRoute, RouterView } from 'vue-router'; // 引入Vue Router核心API
|
||||
import { Layout, Menu, Button, Tabs, Tag, Dropdown, Modal, Tooltip, Spin } from 'ant-design-vue';
|
||||
import { ref, computed, h, reactive, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter, useRoute, RouterView, RouteLocationNormalized } from 'vue-router';
|
||||
import { Layout, Menu, Button, Tabs, Tag, Dropdown, Modal, Tooltip, Spin, message } from 'ant-design-vue';
|
||||
import { getModuleMenus, ModuleItem, MenuItem } from '@/api/menu';
|
||||
import { getUserName } from '@/utils/user';
|
||||
import { apiLogout } from '@/api/auth'
|
||||
|
||||
// 图标导入(无修改)
|
||||
import {
|
||||
ApiOutlined, UserOutlined, CloseOutlined, LinkOutlined, LogoutOutlined,
|
||||
ExclamationCircleOutlined, BarChartOutlined, DownloadOutlined, SettingOutlined,
|
||||
FormOutlined, HistoryOutlined, HomeOutlined, DatabaseOutlined, SearchOutlined,
|
||||
StarOutlined, BellOutlined, QuestionCircleOutlined, MenuFoldOutlined,
|
||||
MenuUnfoldOutlined, CaretUpOutlined, CaretDownOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
// 关键修改1:导入封装的图标库(全局注册的基础,动态图标从这里获取)
|
||||
import icons from '@/icons';
|
||||
import type { AntdIcon } from '@/icons'; // 导入图标类型,确保TypeScript提示
|
||||
|
||||
// 1. 初始化路由实例
|
||||
// 路由实例
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 2. 状态管理(移除原component相关状态,新增路由关联状态)
|
||||
const userName = computed(() => getUserName());
|
||||
|
||||
// 登录态校验守卫
|
||||
const removeAuthGuard = router.beforeEach((to, from, next) => {
|
||||
const userInfo = localStorage.getItem('userInfo');
|
||||
// 未登录且访问非登录页时跳转登录
|
||||
if (to.path !== '/login' && !userInfo) {
|
||||
next('/login');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
// 状态管理
|
||||
const collapsed = ref(false);
|
||||
const logoutVisible = ref(false);
|
||||
const selectedKey = ref(''); // 选中的菜单Code
|
||||
const expandedParentMenus = ref<string[]>([]); // 展开的父菜单Code
|
||||
const loading = ref(true); // 菜单加载状态
|
||||
const contentLoading = ref(false); // 路由切换加载状态
|
||||
const componentError = ref(false); // 组件加载错误
|
||||
const errorPath = ref(''); // 错误路由路径
|
||||
const selectedKey = ref('');
|
||||
const expandedParentMenus = ref<string[]>([]);
|
||||
const loading = ref(true);
|
||||
const componentError = ref(false);
|
||||
const errorPath = ref('');
|
||||
const isNavigating = ref(false);
|
||||
|
||||
// 菜单配置与折叠状态
|
||||
const showFavorite = ref(false)
|
||||
const showNotification = ref(false)
|
||||
const showHelp = ref(false)
|
||||
const showSettings = ref(false)
|
||||
const showUserProfile = ref(false);
|
||||
const showChangePwd = ref(false);
|
||||
|
||||
// 菜单配置
|
||||
const moduleMenusConfig = ref<ModuleItem[]>([]);
|
||||
const collapsedMenuVisible = reactive<Record<string, boolean>>({});
|
||||
|
||||
// 3. 标签页数据结构修改:关联路由Path,移除component
|
||||
// 标签页数据结构(初始路径为/index/console)
|
||||
interface TabItem {
|
||||
key: string; // 建议用menuCode(唯一)
|
||||
title: string; // 菜单名称
|
||||
path: string; // 路由Path(原chref)
|
||||
closable: boolean; // 是否可关闭
|
||||
key: string;
|
||||
title: string;
|
||||
path: string; // 统一为/index/xxx格式
|
||||
closable: boolean;
|
||||
}
|
||||
|
||||
// 初始化标签页:控制台对应路由/console
|
||||
const allTabs = ref<TabItem[]>([
|
||||
{ key: 'console', title: '控制台', path: '/console', closable: false }
|
||||
{ key: 'console', title: '控制台', path: '/index/console', closable: false }
|
||||
]);
|
||||
|
||||
// 4. 计算属性(标签页过滤与索引)
|
||||
// 计算属性
|
||||
const otherTabs = computed(() => allTabs.value.filter(tab => tab.key !== 'console'));
|
||||
const activeTabKey = computed({
|
||||
get() { // 从当前路由同步activeTabKey
|
||||
get() {
|
||||
const matchedTab = allTabs.value.find(tab => tab.path === route.path);
|
||||
return matchedTab ? matchedTab.key : 'console';
|
||||
},
|
||||
set(key) { // 避免手动修改(实际通过路由切换触发)
|
||||
set(key) {
|
||||
const matchedTab = allTabs.value.find(tab => tab.key === key);
|
||||
if (matchedTab) router.push(matchedTab.path);
|
||||
if (matchedTab && !isNavigating.value) {
|
||||
navigateToPath(matchedTab.path);
|
||||
}
|
||||
}
|
||||
});
|
||||
const currentTabIndex = computed(() => {
|
||||
return otherTabs.value.findIndex(tab => tab.path === route.path);
|
||||
});
|
||||
|
||||
// 5. 菜单数据加载(无修改)
|
||||
// 菜单数据加载(标准化chref为/index/xxx)
|
||||
const fetchMenuData = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const modules = await getModuleMenus();
|
||||
moduleMenusConfig.value = modules;
|
||||
// 初始化折叠菜单状态
|
||||
modules.forEach(module => collapsedMenuVisible[module.moduleCode] = false);
|
||||
// 路由初始化后同步菜单状态
|
||||
syncMenuStateWithRoute();
|
||||
// 标准化菜单路径:确保为/index/xxx格式
|
||||
const normalizedModules = modules.map(module => ({
|
||||
...module,
|
||||
menus: module.menus.map(menu => ({
|
||||
...menu,
|
||||
chref: menu.chref
|
||||
? menu.chref.startsWith('/index')
|
||||
? menu.chref
|
||||
: `/index${menu.chref.startsWith('/') ? menu.chref : `/${menu.chref}`}`
|
||||
: ''
|
||||
}))
|
||||
}));
|
||||
moduleMenusConfig.value = normalizedModules;
|
||||
normalizedModules.forEach(module => collapsedMenuVisible[module.moduleCode] = false);
|
||||
syncMenuStateWithRoute(route);
|
||||
} catch (error) {
|
||||
console.error('菜单接口请求失败:', error);
|
||||
Modal.error({ title: '菜单加载失败', content: '请刷新页面重试' });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchMenuData();
|
||||
// 监听路由变化,同步菜单和标签页状态
|
||||
router.beforeEach((to, from, next) => {
|
||||
contentLoading.value = true; // 路由开始切换,显示加载
|
||||
componentError.value = false; // 重置错误状态
|
||||
next();
|
||||
});
|
||||
router.afterEach(() => {
|
||||
contentLoading.value = false; // 路由切换完成,隐藏加载
|
||||
syncMenuStateWithRoute(); // 同步菜单状态
|
||||
});
|
||||
});
|
||||
|
||||
// 6. 核心工具函数
|
||||
// 6.1 图标组件获取(无修改)
|
||||
const getIconComponent = (iconName: string) => {
|
||||
const iconMap: Record<string, any> = {
|
||||
ApiOutlined, UserOutlined, BarChartOutlined, DownloadOutlined,
|
||||
SettingOutlined, FormOutlined, HistoryOutlined, HomeOutlined,
|
||||
DatabaseOutlined, LinkOutlined
|
||||
};
|
||||
return iconMap[iconName] || ApiOutlined;
|
||||
// 路由跳转封装
|
||||
const navigateToPath = async (path: string) => {
|
||||
if (isNavigating.value || path === route.path) return;
|
||||
try {
|
||||
isNavigating.value = true;
|
||||
await router.push(path);
|
||||
} catch (err) {
|
||||
console.error('路由跳转失败:', err);
|
||||
Modal.error({ title: '页面跳转失败', content: `目标路径不存在:${path}` });
|
||||
} finally {
|
||||
isNavigating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 6.2 同步菜单状态与当前路由(关键:路由变 -> 菜单变)
|
||||
const syncMenuStateWithRoute = () => {
|
||||
if (route.path === '/console') { // 控制台路由
|
||||
// 同步菜单与路由状态
|
||||
const syncMenuStateWithRoute = (currentRoute: RouteLocationNormalized) => {
|
||||
// 匹配控制台路径/index/console
|
||||
if (currentRoute.path === '/index/console') {
|
||||
selectedKey.value = '';
|
||||
expandedParentMenus.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 非控制台路由:找到对应菜单,更新选中和展开状态
|
||||
// 匹配其他菜单路由
|
||||
let matched = false;
|
||||
for (const module of moduleMenusConfig.value) {
|
||||
const matchedMenu = module.menus.find(menu => menu.chref === route.path);
|
||||
if (matchedMenu) {
|
||||
selectedKey.value = matchedMenu.menuCode;
|
||||
expandedParentMenus.value = [module.moduleCode]; // 展开当前菜单的父级
|
||||
const menu = module.menus.find(item => item.chref === currentRoute.path);
|
||||
if (menu) {
|
||||
selectedKey.value = menu.menuCode;
|
||||
expandedParentMenus.value = [module.moduleCode];
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 未匹配时跳回控制台
|
||||
if (!matched && currentRoute.path !== '/index/console') {
|
||||
navigateToPath('/index/console');
|
||||
}
|
||||
};
|
||||
|
||||
// 6.3 检查父菜单是否展开(无修改)
|
||||
// 关键修改2:重写getIconComponent,从封装的图标库获取图标(不再依赖局部导入)
|
||||
const getIconComponent = (iconName: string) => {
|
||||
// 图标名称格式校验(确保是有效的Antd图标名称,如"ApiOutlined")
|
||||
const validIconName = iconName as AntdIcon;
|
||||
// 从封装的图标库中获取,不存在则返回默认图标(ApiOutlined)
|
||||
return icons[validIconName] || icons.ApiOutlined;
|
||||
};
|
||||
|
||||
// 检查父菜单是否展开
|
||||
const isSubMenuOpen = (moduleCode: string) => {
|
||||
return expandedParentMenus.value.includes(moduleCode);
|
||||
};
|
||||
|
||||
// 7. 菜单交互逻辑(修改为路由跳转)
|
||||
// 7.1 展开状态:切换父菜单展开/关闭
|
||||
// 菜单交互
|
||||
const toggleSubMenu = (moduleCode: string) => {
|
||||
expandedParentMenus.value = isSubMenuOpen(moduleCode) ? [] : [moduleCode];
|
||||
};
|
||||
|
||||
// 7.2 折叠状态:控制悬浮菜单显示
|
||||
const handleCollapsedMenuVisible = (visible: boolean, moduleCode: string) => {
|
||||
Object.keys(collapsedMenuVisible).forEach(key => {
|
||||
if (key !== moduleCode) collapsedMenuVisible[key] = false;
|
||||
@@ -371,38 +448,31 @@ const handleCollapsedMenuVisible = (visible: boolean, moduleCode: string) => {
|
||||
collapsedMenuVisible[moduleCode] = visible;
|
||||
};
|
||||
|
||||
// 7.3 展开状态:子菜单点击(核心:跳转路由 + 添加标签页)
|
||||
const handleSubMenuItemClick = (menu: MenuItem) => {
|
||||
const targetPath = menu.chref; // 菜单的chref即路由Path
|
||||
if (!targetPath) return;
|
||||
|
||||
// 步骤1:跳转路由
|
||||
router.push(targetPath);
|
||||
|
||||
// 步骤2:添加标签页(不存在则新增)
|
||||
const tabExists = allTabs.value.some(tab => tab.path === targetPath);
|
||||
if (!menu.chref) return;
|
||||
navigateToPath(menu.chref);
|
||||
|
||||
// 添加标签页
|
||||
const tabExists = allTabs.value.some(tab => tab.path === menu.chref);
|
||||
if (!tabExists) {
|
||||
allTabs.value.push({
|
||||
key: menu.menuCode,
|
||||
title: menu.menuName,
|
||||
path: targetPath,
|
||||
path: menu.chref,
|
||||
closable: true
|
||||
});
|
||||
}
|
||||
|
||||
// 步骤3:关闭折叠菜单(若存在)
|
||||
|
||||
Object.keys(collapsedMenuVisible).forEach(key => collapsedMenuVisible[key] = false);
|
||||
};
|
||||
|
||||
// 7.4 折叠状态:悬浮子菜单点击(同展开状态逻辑)
|
||||
const handleCollapsedSubMenuClick = (e: { key: string }, menus: MenuItem[]) => {
|
||||
const menuCode = e.key;
|
||||
const menu = menus.find(item => item.menuCode === menuCode);
|
||||
if (menu) handleSubMenuItemClick(menu);
|
||||
};
|
||||
|
||||
// 8. 标签页交互逻辑(与路由绑定)
|
||||
// 8.1 渲染标签标题(无修改)
|
||||
// 标签页交互
|
||||
const renderTabTitle = (tab: TabItem) => {
|
||||
return h('div', { class: 'tab-title-container' }, [
|
||||
h('span', { class: 'tab-title-text' }, tab.title),
|
||||
@@ -415,18 +485,17 @@ const renderTabTitle = (tab: TabItem) => {
|
||||
deleteTab(tab);
|
||||
}
|
||||
},
|
||||
h(CloseOutlined, { size: 12 })
|
||||
// 关键修改:用 getIconComponent 获取 CloseOutlined 图标
|
||||
h(getIconComponent('CloseOutlined'), { size: 12 })
|
||||
)
|
||||
]);
|
||||
};
|
||||
|
||||
// 8.2 切换到控制台(跳转路由)
|
||||
const switchToConsole = () => {
|
||||
router.push('/console');
|
||||
navigateToPath('/index/console');
|
||||
Object.keys(collapsedMenuVisible).forEach(key => collapsedMenuVisible[key] = false);
|
||||
};
|
||||
|
||||
// 8.3 标签页切换(上一个/下一个)
|
||||
const switchTab = (direction: 'prev' | 'next') => {
|
||||
const tabs = otherTabs.value;
|
||||
if (tabs.length === 0) return;
|
||||
@@ -435,79 +504,167 @@ const switchTab = (direction: 'prev' | 'next') => {
|
||||
let targetIdx = direction === 'prev' ? currentIdx - 1 : currentIdx + 1;
|
||||
targetIdx = Math.max(0, Math.min(targetIdx, tabs.length - 1));
|
||||
|
||||
router.push(tabs[targetIdx].path); // 跳转目标路由
|
||||
navigateToPath(tabs[targetIdx].path);
|
||||
};
|
||||
|
||||
// 8.4 关闭单个标签页
|
||||
const deleteTab = (tab: TabItem) => {
|
||||
if (!tab.closable) return;
|
||||
|
||||
const tabIndex = allTabs.value.findIndex(item => item.key === tab.key);
|
||||
if (tabIndex === -1) return;
|
||||
|
||||
// 若关闭的是当前激活标签,跳转到前一个标签或控制台
|
||||
const isActive = tab.path === route.path;
|
||||
allTabs.value = allTabs.value.filter(item => item.key !== tab.key);
|
||||
|
||||
if (isActive) {
|
||||
const tabs = otherTabs.value;
|
||||
if (tabs.length > 0) {
|
||||
const targetTab = tabs[Math.min(tabIndex - 1, tabs.length - 1)];
|
||||
router.push(targetTab.path);
|
||||
const targetTab = tabs[tabIndex - 1] || tabs[tabIndex];
|
||||
navigateToPath(targetTab.path);
|
||||
} else {
|
||||
router.push('/console');
|
||||
navigateToPath('/index/console');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 8.5 关闭所有标签页
|
||||
const closeAllTabs = () => {
|
||||
allTabs.value = allTabs.value.filter(tab => tab.key === 'console');
|
||||
router.push('/console');
|
||||
navigateToPath('/index/console');
|
||||
Object.keys(collapsedMenuVisible).forEach(key => collapsedMenuVisible[key] = false);
|
||||
};
|
||||
|
||||
// 8.6 标签页切换事件(路由已同步,无需额外处理)
|
||||
const handleTabChange = (key: string) => {
|
||||
const matchedTab = allTabs.value.find(tab => tab.key === key);
|
||||
if (matchedTab) router.push(matchedTab.path);
|
||||
if (matchedTab) navigateToPath(matchedTab.path);
|
||||
};
|
||||
|
||||
// 9. 错误处理:RouterView组件加载失败
|
||||
// 错误处理
|
||||
const handleRouterViewError = (err: Error) => {
|
||||
componentError.value = true;
|
||||
errorPath.value = route.path;
|
||||
console.error('路由组件加载失败:', err);
|
||||
};
|
||||
|
||||
// 10. 用户菜单与退出登录(优化退出跳转)
|
||||
const reloadCurrentRoute = () => {
|
||||
componentError.value = false;
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: { ...route.query, t: Date.now() }
|
||||
});
|
||||
};
|
||||
|
||||
// 用户菜单与退出登录
|
||||
const handleDropdownVisible = (visible: boolean) => {
|
||||
console.log('用户下拉菜单状态:', visible ? '显示' : '隐藏');
|
||||
};
|
||||
|
||||
const handleUserMenuClick = ({ key }: { key: string }) => {
|
||||
if (key === 'logout') handleLogout();
|
||||
else console.log('用户菜单点击:', key);
|
||||
switch (key) {
|
||||
case 'profile':
|
||||
showUserProfile.value = true;
|
||||
break;
|
||||
case 'settings':
|
||||
showChangePwd.value = true;
|
||||
break;
|
||||
case 'logout':
|
||||
handleLogout();
|
||||
break;
|
||||
default:
|
||||
console.log('用户菜单点击:', key);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logoutVisible.value = true;
|
||||
};
|
||||
|
||||
const doLogout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('userInfo');
|
||||
logoutVisible.value = false;
|
||||
router.push('/login'); // 用路由跳转替代alert
|
||||
|
||||
const doLogout = async () => {
|
||||
try {
|
||||
// 1. 调用后端登出接口(可选)
|
||||
const response = await apiLogout();
|
||||
if (response.code == 200 ){
|
||||
// 2. 清掉所有缓存
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
logoutVisible.value = false;
|
||||
window.location.href = `${import.meta.env.BASE_URL}login`;
|
||||
}
|
||||
} catch (e) {
|
||||
message.error('退出失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 11. 监听菜单折叠状态变化
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchMenuData();
|
||||
router.afterEach((to) => {
|
||||
componentError.value = false;
|
||||
syncMenuStateWithRoute(to);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
removeAuthGuard();
|
||||
});
|
||||
|
||||
watch(collapsed, (newVal) => {
|
||||
if (newVal) expandedParentMenus.value = []; // 折叠时关闭所有父菜单
|
||||
else syncMenuStateWithRoute(); // 展开时同步路由状态
|
||||
if (newVal) expandedParentMenus.value = [];
|
||||
else syncMenuStateWithRoute(route);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import "styles/app-layout.css";
|
||||
|
||||
.main-content {
|
||||
padding: 16px;
|
||||
height: calc(100% - 48px);
|
||||
overflow: auto;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.content-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.component-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #f5222d;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-desc, .error-path {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.sub-menu-item-disabled {
|
||||
color: #ccc !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
</style>
|
||||
28
capi-ui/src/components/Layout/ChangePwd.vue
Normal file
28
capi-ui/src/components/Layout/ChangePwd.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="修改密码"
|
||||
:width="modalWidth"
|
||||
:centered="true"
|
||||
:body-style="bodyStyle"
|
||||
:footer="null"
|
||||
@cancel="$emit('close')">
|
||||
<p>这里是个人资料表单内容……</p>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const modalWidth = computed(() => window.innerWidth * 0.6);
|
||||
const bodyStyle = computed(() => ({
|
||||
height: `${window.innerHeight * 0.6}px`,
|
||||
overflowY: 'auto'
|
||||
}));
|
||||
</script>
|
||||
28
capi-ui/src/components/Layout/UserProfile.vue
Normal file
28
capi-ui/src/components/Layout/UserProfile.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="个人资料"
|
||||
:width="modalWidth"
|
||||
:centered="true"
|
||||
:body-style="bodyStyle"
|
||||
:footer="null"
|
||||
@cancel="$emit('close')">
|
||||
<p>这里是个人资料表单内容……</p>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const modalWidth = computed(() => window.innerWidth * 0.6);
|
||||
const bodyStyle = computed(() => ({
|
||||
height: `${window.innerHeight * 0.6}px`,
|
||||
overflowY: 'auto'
|
||||
}));
|
||||
</script>
|
||||
28
capi-ui/src/components/dialog/Favorite.vue
Normal file
28
capi-ui/src/components/dialog/Favorite.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="收藏"
|
||||
:width="modalWidth"
|
||||
:centered="true"
|
||||
:body-style="bodyStyle"
|
||||
:footer="null"
|
||||
@cancel="$emit('close')">
|
||||
<p>这里是个人资料表单内容……</p>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const modalWidth = computed(() => window.innerWidth * 0.6);
|
||||
const bodyStyle = computed(() => ({
|
||||
height: `${window.innerHeight * 0.6}px`,
|
||||
overflowY: 'auto'
|
||||
}));
|
||||
</script>
|
||||
28
capi-ui/src/components/dialog/Help.vue
Normal file
28
capi-ui/src/components/dialog/Help.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="帮助"
|
||||
:width="modalWidth"
|
||||
:centered="true"
|
||||
:body-style="bodyStyle"
|
||||
:footer="null"
|
||||
@cancel="$emit('close')">
|
||||
<p>这里是个人资料表单内容……</p>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const modalWidth = computed(() => window.innerWidth * 0.6);
|
||||
const bodyStyle = computed(() => ({
|
||||
height: `${window.innerHeight * 0.6}px`,
|
||||
overflowY: 'auto'
|
||||
}));
|
||||
</script>
|
||||
28
capi-ui/src/components/dialog/Notification.vue
Normal file
28
capi-ui/src/components/dialog/Notification.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="通知"
|
||||
:width="modalWidth"
|
||||
:centered="true"
|
||||
:body-style="bodyStyle"
|
||||
:footer="null"
|
||||
@cancel="$emit('close')">
|
||||
<p>这里是个人资料表单内容……</p>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const modalWidth = computed(() => window.innerWidth * 0.6);
|
||||
const bodyStyle = computed(() => ({
|
||||
height: `${window.innerHeight * 0.6}px`,
|
||||
overflowY: 'auto'
|
||||
}));
|
||||
</script>
|
||||
28
capi-ui/src/components/dialog/Settings.vue
Normal file
28
capi-ui/src/components/dialog/Settings.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="设置"
|
||||
:width="modalWidth"
|
||||
:centered="true"
|
||||
:body-style="bodyStyle"
|
||||
:footer="null"
|
||||
@cancel="$emit('close')">
|
||||
<p>这里是个人资料表单内容……</p>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const modalWidth = computed(() => window.innerWidth * 0.6);
|
||||
const bodyStyle = computed(() => ({
|
||||
height: `${window.innerHeight * 0.6}px`,
|
||||
overflowY: 'auto'
|
||||
}));
|
||||
</script>
|
||||
@@ -1,6 +1,4 @@
|
||||
<template>
|
||||
|
||||
ffffffffffffffffffff
|
||||
</template>
|
||||
|
||||
<script>
|
||||
21
capi-ui/src/icons/index.ts
Normal file
21
capi-ui/src/icons/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// 导入所有Ant Design图标
|
||||
import * as AllIcons from '@ant-design/icons-vue';
|
||||
|
||||
// 过滤掉非图标组件
|
||||
const exclude = [
|
||||
'createFromIconfontCN',
|
||||
'getTwoToneColor',
|
||||
'setTwoToneColor',
|
||||
'default'
|
||||
];
|
||||
|
||||
// 提取并导出所有图标组件
|
||||
const icons = Object.entries(AllIcons)
|
||||
.filter(([name]) => !exclude.includes(name))
|
||||
.reduce((acc, [name, component]) => {
|
||||
acc[name] = component;
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
|
||||
export default icons;
|
||||
export type AntdIcon = keyof typeof icons;
|
||||
@@ -5,14 +5,16 @@ import router from './router' // 新增
|
||||
import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import 'ant-design-vue/dist/reset.css'
|
||||
import icons from './icons'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
|
||||
app.use(createPinia()) // 注册Pinia
|
||||
app.use(router) // 注册路由
|
||||
app.use(pinia);
|
||||
app.use(Antd) // 注册Ant Design Vue
|
||||
Object.entries(icons).forEach(([name, component]) => {
|
||||
app.component(name, component)
|
||||
})
|
||||
app.mount('#app')
|
||||
@@ -1,16 +1,23 @@
|
||||
// router/index.ts
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
// 1. 以相对路径写 glob(相对于当前文件)
|
||||
const Login = () => import('@/views/login/Login.vue');
|
||||
const Home = () => import('@/components/Layout/Main.vue');
|
||||
const Index = () => import('@/views/sys/index.vue');
|
||||
const NotFound = () => import('@/views/sys/NotFound.vue');
|
||||
|
||||
// 1. 自动导入biz模块路由
|
||||
const bizModules = import.meta.glob('../views/biz/**/*.vue')
|
||||
|
||||
// 2. 根据模块生成子路由
|
||||
// 2. 生成biz子路由(保留index后缀)
|
||||
function buildBizRoutes() {
|
||||
return Object.entries(bizModules).map(([filePath, asyncComp]) => {
|
||||
const routePath = filePath
|
||||
.replace(/^..\views\/biz/, '')
|
||||
.replace(/\.vue$/, '')
|
||||
.replace(/\/index$/i, '') || '/'
|
||||
let routePath = filePath
|
||||
.replace(/^..\/views/, '') // 保留biz及子目录
|
||||
.replace(/\.vue$/, ''); // 不移除index后缀
|
||||
|
||||
// 处理路径开头的斜杠
|
||||
if (!routePath) routePath = '';
|
||||
else if (routePath.startsWith('/')) routePath = routePath.slice(1);
|
||||
|
||||
const routeName = routePath
|
||||
.split('/')
|
||||
@@ -27,33 +34,43 @@ function buildBizRoutes() {
|
||||
})
|
||||
}
|
||||
|
||||
// 3. 固定路由
|
||||
// 3. 路由配置(核心:将NotFound作为/index的子路由)
|
||||
const routes = [
|
||||
{ path: '/', redirect: '/login' },
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../views/login/Login.vue')
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/index',
|
||||
path: '/index', // 父路由
|
||||
name: 'Index',
|
||||
component: () => import('../views/sys/index.vue'),
|
||||
component: Index, // 包含内容区RouterView
|
||||
meta: { requiresAuth: true },
|
||||
children: buildBizRoutes()
|
||||
children: [
|
||||
...buildBizRoutes(), // 业务路由
|
||||
{ path: 'console', component: Home, name: 'Console' },
|
||||
{ path: '', redirect: 'console' },
|
||||
// 关键:NotFound作为子路由,匹配/index下的所有未定义路径
|
||||
{
|
||||
path: ':pathMatch(.*)*', // 子路由通配符
|
||||
name: 'IndexNotFound',
|
||||
component: NotFound
|
||||
}
|
||||
]
|
||||
},
|
||||
// 顶层通配符:将非/index的无效路径重定向到/index下的NotFound
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('../views/sys/NotFound.vue'),
|
||||
meta: { hidden: true }
|
||||
redirect: to => {
|
||||
return `/index/${to.params.pathMatch.join('/')}`;
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 4. 创建路由实例
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
export default router
|
||||
|
||||
@@ -1,4 +1,32 @@
|
||||
import { createPinia } from 'pinia';
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
const pinia = createPinia();
|
||||
export default pinia;
|
||||
// 定义存储的状态类型
|
||||
interface AuthState {
|
||||
token: string | null
|
||||
userInfo: any | null // 可根据实际用户信息结构定义具体类型
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: (): AuthState => ({
|
||||
// 初始化时从localStorage读取,保证刷新后状态不丢失
|
||||
token: localStorage.getItem('token') || null,
|
||||
userInfo: localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')!) : null
|
||||
}),
|
||||
actions: {
|
||||
// 保存登录状态(登录成功时调用)
|
||||
setAuthInfo(token: string, userInfo: any) {
|
||||
this.token = token
|
||||
this.userInfo = userInfo
|
||||
// 同步到localStorage持久化
|
||||
localStorage.setItem('token', token)
|
||||
localStorage.setItem('userInfo', JSON.stringify(userInfo))
|
||||
},
|
||||
// 清除登录状态(退出登录或token失效时调用)
|
||||
clearAuthInfo() {
|
||||
this.token = null
|
||||
this.userInfo = null
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
13
capi-ui/src/utils/user.ts
Normal file
13
capi-ui/src/utils/user.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// utils/user.ts
|
||||
export function getUserName(): string {
|
||||
try {
|
||||
const userInfo = localStorage.getItem('userInfo');
|
||||
if (userInfo) {
|
||||
const parsed = JSON.parse(userInfo);
|
||||
return parsed.uname || '用户';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取用户信息失败:', e);
|
||||
}
|
||||
return '访客';
|
||||
}
|
||||
273
capi-ui/src/views/biz/app/icon.vue
Normal file
273
capi-ui/src/views/biz/app/icon.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<div class="icon-gallery" :style="containerStyle">
|
||||
<!-- 搜索框 -->
|
||||
<a-input
|
||||
v-if="showSearch"
|
||||
v-model:value="keyword"
|
||||
placeholder="搜索图标名称..."
|
||||
allow-clear
|
||||
class="search-input"
|
||||
:size="searchSize"
|
||||
/>
|
||||
|
||||
<!-- 图标网格 -->
|
||||
<div class="icons-grid" :style="gridStyle">
|
||||
<div
|
||||
v-for="item in filteredIcons"
|
||||
:key="item.name"
|
||||
class="icon-item"
|
||||
:style="itemStyle"
|
||||
@mouseenter="handleMouseEnter(item.name)"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<component :is="item.component" class="icon" :style="iconStyle" />
|
||||
<span class="icon-name">{{ item.name }}</span>
|
||||
|
||||
<!-- 复制按钮 - 仅在hover时显示 -->
|
||||
<div v-if="hoveredIcon === item.name" class="copy-buttons">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click.stop="copyWithTags(item.name)"
|
||||
class="copy-btn"
|
||||
>
|
||||
<CopyOutlined class="mr-1" />标签
|
||||
</a-button>
|
||||
<a-button
|
||||
type="default"
|
||||
size="small"
|
||||
@click.stop="copyName(item.name)"
|
||||
class="copy-btn"
|
||||
>
|
||||
<CopyOutlined class="mr-1" />名称
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredIcons.length === 0" class="empty-state">
|
||||
<a-empty description="没有找到匹配的图标" />
|
||||
</div>
|
||||
|
||||
<!-- 复制提示 -->
|
||||
<a-message
|
||||
v-if="showMessage"
|
||||
:content="messageContent"
|
||||
:type="messageType"
|
||||
:duration="messageDuration"
|
||||
class="copy-message"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, PropType, onMounted } from 'vue'
|
||||
import * as Icons from '@ant-design/icons-vue'
|
||||
import { Input, Button, Empty, Message } from 'ant-design-vue'
|
||||
import { CopyOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
// 是否显示搜索框
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 搜索框尺寸
|
||||
searchSize: {
|
||||
type: String as PropType<'small' | 'middle' | 'large'>,
|
||||
default: 'middle'
|
||||
},
|
||||
// 网格列数
|
||||
columns: {
|
||||
type: Number,
|
||||
default: 6
|
||||
},
|
||||
// 图标大小
|
||||
iconSize: {
|
||||
type: Number,
|
||||
default: 24
|
||||
},
|
||||
// 每个图标项的内边距
|
||||
itemPadding: {
|
||||
type: String,
|
||||
default: '16px'
|
||||
},
|
||||
// 复制提示消息持续时间(毫秒)
|
||||
messageDuration: {
|
||||
type: Number,
|
||||
default: 2000
|
||||
}
|
||||
})
|
||||
|
||||
// 过滤掉非图标成员
|
||||
const exclude = [
|
||||
'createFromIconfontCN',
|
||||
'getTwoToneColor',
|
||||
'setTwoToneColor',
|
||||
'default',
|
||||
'CopyOutlined' // 排除我们在按钮中使用的复制图标
|
||||
]
|
||||
|
||||
// 生成图标列表
|
||||
const iconList = Object.entries(Icons)
|
||||
.filter(([name]) => !exclude.includes(name))
|
||||
.map(([name, component]) => ({ name, component }))
|
||||
|
||||
// 搜索相关
|
||||
const keyword = ref('')
|
||||
const filteredIcons = computed(() => {
|
||||
if (!keyword.value) return iconList
|
||||
|
||||
return iconList.filter(({ name }) =>
|
||||
name.toLowerCase().includes(keyword.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
// 交互状态
|
||||
const hoveredIcon = ref('')
|
||||
const showMessage = ref(false)
|
||||
const messageContent = ref('')
|
||||
const messageType = ref<'success' | 'error'>('success')
|
||||
|
||||
// 处理鼠标悬停
|
||||
function handleMouseEnter(iconName: string) {
|
||||
hoveredIcon.value = iconName
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
hoveredIcon.value = ''
|
||||
}
|
||||
|
||||
// 复制功能
|
||||
function copyWithTags(name: string) {
|
||||
const code = `<${name} />`
|
||||
copyToClipboard(code)
|
||||
.then(() => {
|
||||
showMessageToast(`已复制: ${code}`, 'success')
|
||||
})
|
||||
.catch(() => {
|
||||
showMessageToast('复制失败,请手动复制', 'error')
|
||||
})
|
||||
}
|
||||
|
||||
function copyName(name: string) {
|
||||
copyToClipboard(name)
|
||||
.then(() => {
|
||||
showMessageToast(`已复制: ${name}`, 'success')
|
||||
})
|
||||
.catch(() => {
|
||||
showMessageToast('复制失败,请手动复制', 'error')
|
||||
})
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
async function copyToClipboard(text: string): Promise<boolean> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('无法复制文本: ', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示消息提示
|
||||
function showMessageToast(content: string, type: 'success' | 'error') {
|
||||
messageContent.value = content
|
||||
messageType.value = type
|
||||
showMessage.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
showMessage.value = false
|
||||
}, props.messageDuration)
|
||||
}
|
||||
|
||||
// 计算样式
|
||||
const containerStyle = computed(() => ({
|
||||
padding: '16px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '4px'
|
||||
}))
|
||||
|
||||
const gridStyle = computed(() => ({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${props.columns}, 1fr)`,
|
||||
gap: '12px',
|
||||
marginTop: props.showSearch ? '16px' : 0
|
||||
}))
|
||||
|
||||
const itemStyle = computed(() => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '6px',
|
||||
padding: props.itemPadding,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s'
|
||||
}))
|
||||
|
||||
const iconStyle = computed(() => ({
|
||||
fontSize: `${props.iconSize}px`,
|
||||
color: '#262626',
|
||||
marginBottom: '8px'
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon-gallery {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.icons-grid {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.icon-item:hover {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.icon-name {
|
||||
font-size: 12px;
|
||||
color: #595959;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.copy-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
padding: 2px 8px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.copy-message {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
9
capi-ui/src/views/biz/app/index.vue
Normal file
9
capi-ui/src/views/biz/app/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
app
|
||||
</template>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
61
capi-ui/src/views/biz/data/Apply.vue
Normal file
61
capi-ui/src/views/biz/data/Apply.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="申请数据源"
|
||||
:width="modalWidth"
|
||||
:centered="true"
|
||||
:body-style="bodyStyle"
|
||||
@cancel="$emit('close')"
|
||||
@ok="$emit('confirm')"
|
||||
>
|
||||
<div class="apply-modal-content">
|
||||
<div class="data-name-section">
|
||||
<span class="label">申请数据源:</span>
|
||||
<span class="value">{{ taskName }} {{ taskId }}</span>
|
||||
</div>
|
||||
<div class="apply-form-section">
|
||||
<a-form-item label="申请理由" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
|
||||
<a-input
|
||||
placeholder="请说明申请该数据的用途"
|
||||
rows="4"
|
||||
type="textarea"
|
||||
v-model:value="applyReason"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="申请期限" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
|
||||
<a-date-picker placeholder="选择申请截止日期" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, defineProps, defineEmits } from 'vue';
|
||||
import { Form, Input, DatePicker, Modal } from 'ant-design-vue';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
taskName: string;
|
||||
taskId: string ;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(['close', 'confirm']);
|
||||
|
||||
const modalWidth = computed(() => window.innerWidth * 0.6);
|
||||
const bodyStyle = computed(() => ({
|
||||
height: `${window.innerHeight * 0.6}px`,
|
||||
overflowY: 'auto'
|
||||
}));
|
||||
|
||||
const applyReason = ref('');
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.apply-modal-content .data-name-section { margin-bottom: 10px; }
|
||||
.apply-modal-content .data-name-section .label { color: #666; font-weight: 500; margin-right: 8px; }
|
||||
.apply-modal-content .data-name-section .value { color: #1890ff; font-weight: 500; }
|
||||
.apply-modal-content .apply-form-section { margin-top: 20px; }
|
||||
.apply-modal-content .apply-form-section .ant-form-item { margin-bottom: 16px; }
|
||||
</style>
|
||||
51
capi-ui/src/views/biz/data/NameDetai.vue
Normal file
51
capi-ui/src/views/biz/data/NameDetai.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="数据源详情"
|
||||
:width="modalWidth"
|
||||
:centered="true"
|
||||
:body-style="bodyStyle"
|
||||
:footer="null"
|
||||
@cancel="$emit('close')"
|
||||
>
|
||||
<div class="name-detail-content">
|
||||
<div class="detail-header">
|
||||
<h3 style="margin: 0;">{{ taskName }} {{ taskId }} 详情</h3>
|
||||
</div>
|
||||
<div class="detail-list">
|
||||
<div class="detail-item"><span class="label">数据源名称:</span><span class="value">{{ taskName }}</span></div>
|
||||
<div class="detail-item"><span class="label">数据类型:</span><span class="value">结构化数据表(MySQL)</span></div>
|
||||
<div class="detail-item"><span class="label">数据更新频率:</span><span class="value">每日增量更新</span></div>
|
||||
<div class="detail-item"><span class="label">字段数量:</span><span class="value">28个(含主键、时间戳、业务字段)</span></div>
|
||||
<div class="detail-item"><span class="label">访问权限:</span><span class="value">需申请(审批后只读访问)</span></div>
|
||||
<div class="detail-item"><span class="label">备注:</span><span class="value">该表包含核心业务数据,禁止用于非授权场景。</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineProps, defineEmits } from 'vue';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
taskName: string;
|
||||
taskId: string ;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const modalWidth = computed(() => window.innerWidth * 0.6);
|
||||
const bodyStyle = computed(() => ({
|
||||
height: `${window.innerHeight * 0.6}px`,
|
||||
overflowY: 'auto'
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.name-detail-content .detail-header { margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #f0f0f0; }
|
||||
.name-detail-content .detail-list .detail-item { margin-bottom: 12px; display: flex; align-items: flex-start; }
|
||||
.name-detail-content .detail-list .detail-item .label { color: #666; font-weight: 500; width: 120px; flex-shrink: 0; }
|
||||
.name-detail-content .detail-list .detail-item .value { color: #333; flex-grow: 1; }
|
||||
</style>
|
||||
@@ -1,162 +1,219 @@
|
||||
<template>
|
||||
<!-- 根容器:100%填充父容器(home-content),确保自适应 -->
|
||||
<div class="empty-page-container">
|
||||
<!-- 空白页核心内容区:垂直居中 -->
|
||||
<div class="empty-content">
|
||||
<!-- 空状态图标 -->
|
||||
<div class="empty-icon">
|
||||
<FileTextOutlined />
|
||||
</div>
|
||||
<div class="data-list-container">
|
||||
<div class="search-card" :bordered="false">
|
||||
<a-row :gutter="4" align="middle" class="search-row">
|
||||
<!-- 下拉框 -->
|
||||
<a-col :span="3" :sm="4" :xs="24" class="select-col">
|
||||
<a-select
|
||||
placeholder="下拉框"
|
||||
style="width: 100%"
|
||||
:bordered="false"
|
||||
class="custom-select"
|
||||
v-model:value="selectValue"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="option in selectOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:disabled="option.disabled"
|
||||
>
|
||||
{{ option.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<!-- 输入框 -->
|
||||
<a-col :span="15" :sm="14" :xs="24" class="input-col">
|
||||
<a-input
|
||||
placeholder="输入框"
|
||||
style="width: 100%; padding: 0 8px;"
|
||||
:bordered="false"
|
||||
class="custom-input"
|
||||
v-model:value="inputValue"
|
||||
clearable
|
||||
/>
|
||||
</a-col>
|
||||
<!-- 按钮区域 -->
|
||||
<a-col :span="6" :sm="6" :xs="24" class="button-col">
|
||||
<div class="search-buttons">
|
||||
<a-button type="primary" class="search-btn" @click="handleSearch">搜索</a-button>
|
||||
<a-button class="reset-btn" @click="handleReset">重置</a-button>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<div v-else class="data-cards">
|
||||
<a-card class="data-card" v-for="(item, index) in dataList" :key="index" :bordered="true">
|
||||
<a-row class="card-header">
|
||||
<a-col :span="16">
|
||||
<div class="data-name" @click="handleNameClick(item)">{{ item.targetTable }}</div>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<div class="data-storage"><span class="label">数据库: </span>{{ item.dbType }}</div>
|
||||
<div class="data-storage"><span class="label">存储量: </span>{{ item.successRows }} 条</div>
|
||||
</a-col>
|
||||
<a-col :span="4" class="time-info">
|
||||
<div class="create-time">创建时间: {{ item.createTime }}</div>
|
||||
<div class="update-time">更新时间: {{ item.updateTime }}</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row class="card-body">
|
||||
<a-col :span="24">
|
||||
<div class="data-description"><span class="label">描述: </span>{{ item.taskName }}</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row class="card-footer">
|
||||
<a-col :span="24" class="card-actions">
|
||||
<a-button size="small" class="favorite-btn" @click="handleFavoriteToggle(item)" :disabled="item.isActive">
|
||||
{{ item.isActive ? '已收藏' : '收藏' }}
|
||||
</a-button>
|
||||
<a-button size="small" type="primary" class="apply-btn" @click="handleApplyClick(item)">申请</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 标题与描述 -->
|
||||
<h2 class="empty-title">这是一个空白页面</h2>
|
||||
<p class="empty-desc">
|
||||
你可以在这里扩展功能,比如添加数据展示、表单提交、图表分析等内容<br>
|
||||
页面已适配父容器尺寸,支持高度/宽度自适应
|
||||
</p>
|
||||
|
||||
<!-- 操作按钮:提供基础交互入口 -->
|
||||
<div class="empty-actions">
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<PlusOutlined class="mr-2" /> 创建内容
|
||||
</a-button>
|
||||
<a-button @click="handleGuide" style="margin-left: 12px;">
|
||||
<QuestionCircleOutlined class="mr-2" /> 查看使用指南
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 辅助提示:展示页面基础信息 -->
|
||||
<div class="empty-meta">
|
||||
<span class="meta-item">页面路径:@/views/EmptyTemplatePage.vue</span>
|
||||
<span class="meta-item">适配场景:列表页、详情页、表单页等初始模板</span>
|
||||
<!-- 无数据提示 -->
|
||||
<div v-if="dataList && dataList.length === 0" class="no-data">
|
||||
暂无数据
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ApplyModal v-if="applyModalVisible" :visible="applyModalVisible" :task-name="currentItem?.taskName" :task-id="currentItem?.taskId" @close="applyModalVisible = false" @confirm="handleApplyConfirm" />
|
||||
<NameDetailModal v-if="nameModalVisible" :visible="nameModalVisible" :task-name="currentItem?.taskName" :task-id="currentItem?.taskId" @close="nameModalVisible = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
// 引入必要组件(与原项目组件库一致)
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { FileTextOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { Card, Row, Col, Select, Input, Button, Spin } from 'ant-design-vue';
|
||||
import ApplyModal from './Apply.vue';
|
||||
import NameDetailModal from './NameDetai.vue';
|
||||
import { getDbList, DbItem, SyncTables, getSyncTables } from '@/api/data';
|
||||
|
||||
// 页面基础状态(可根据需求扩展)
|
||||
const pageName = ref('空白模板页面');
|
||||
// 搜索框数据绑定
|
||||
const selectValue = ref('');
|
||||
const inputValue = ref('');
|
||||
|
||||
// 按钮交互事件(示例:可根据业务逻辑修改)
|
||||
const handleCreate = () => {
|
||||
console.log(`[${pageName.value}] 触发「创建内容」操作`);
|
||||
// 实际场景可扩展:打开创建弹窗、跳转表单页面等
|
||||
alert('可在此处实现「创建内容」的业务逻辑');
|
||||
const selectOptions = ref<DbItem[]>();
|
||||
const selectLoading = ref(false);
|
||||
|
||||
// 数据列表和加载状态
|
||||
const dataList = ref<SyncTables[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 重置事件
|
||||
const handleReset = () => {
|
||||
selectValue.value = '';
|
||||
inputValue.value = '';
|
||||
// 重置后重新加载数据
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleGuide = () => {
|
||||
console.log(`[${pageName.value}] 查看使用指南`);
|
||||
// 实际场景可扩展:打开帮助文档、显示引导弹窗等
|
||||
alert('使用指南:\n1. 此页面已适配父容器自适应尺寸\n2. 可在 empty-content 内添加业务组件\n3. 样式可参考原项目规范调整');
|
||||
// 搜索事件
|
||||
const handleSearch = () => {
|
||||
fetchData();
|
||||
};
|
||||
|
||||
// 弹窗控制
|
||||
const currentItem = ref<SyncTables | null>(null);
|
||||
const applyModalVisible = ref(false);
|
||||
const nameModalVisible = ref(false);
|
||||
|
||||
// 获取数据列表
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 构建查询参数
|
||||
const params = {
|
||||
dbId: selectValue.value,
|
||||
targetTable: inputValue.value
|
||||
};
|
||||
console.log(params)
|
||||
// 调用接口获取数据
|
||||
const result = await getSyncTables(params);
|
||||
dataList.value = result || [];
|
||||
|
||||
console.log(result)
|
||||
|
||||
} catch (e) {
|
||||
console.error('获取数据失败:', e);
|
||||
dataList.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// 加载下拉框选项
|
||||
selectLoading.value = true;
|
||||
try {
|
||||
selectOptions.value = await getDbList();
|
||||
} catch (e) {
|
||||
console.error('获取数据库列表失败:', e);
|
||||
} finally {
|
||||
selectLoading.value = false;
|
||||
}
|
||||
|
||||
// 页面加载时默认获取数据
|
||||
fetchData();
|
||||
});
|
||||
|
||||
// 事件处理
|
||||
const handleFavoriteToggle = (item: SyncTables) => {
|
||||
item.isFavorite = !item.isFavorite;
|
||||
};
|
||||
const handleApplyClick = (item: SyncTables) => {
|
||||
currentItem.value = item;
|
||||
applyModalVisible.value = true;
|
||||
};
|
||||
const handleNameClick = (item: SyncTables) => {
|
||||
currentItem.value = item;
|
||||
nameModalVisible.value = true;
|
||||
};
|
||||
const handleApplyConfirm = () => {
|
||||
console.log('申请提交:', currentItem.value?.taskName);
|
||||
applyModalVisible.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 根容器:100%填充父容器,确保自适应 */
|
||||
.empty-page-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
<style scoped lang="scss">
|
||||
.data-list-container { padding: 2px; min-height: 80vh; }
|
||||
.search-card { background-color: #f0f7ff; padding: 16px; margin-bottom: 16px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); position: sticky; top: 0; z-index: 10; }
|
||||
.search-row { white-space: nowrap; width: 100%; display: flex; align-items: center; }
|
||||
.select-col, .input-col, .button-col { display: inline-block; vertical-align: middle; }
|
||||
.custom-select, .custom-input { width: 100%; border: 1px solid #d9d9d9; border-radius: 4px; background-color: #fff; height: 32px; }
|
||||
.custom-select .a-select-selector, .custom-input .a-input-inner { border: none !important; box-shadow: none !important; background-color: transparent !important; height: 100% !important; line-height: 32px !important; }
|
||||
.search-buttons { display: flex; gap: 8px; margin: 0; padding: 0; height: 32px; align-items: center; }
|
||||
.search-btn, .reset-btn { height: 32px; padding: 0 16px; }
|
||||
.data-cards { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
|
||||
.data-card { background-color: #fff; border-radius: 4px; overflow: hidden; transition: all 0.3s ease; }
|
||||
.data-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.1); transform: translateY(-2px); }
|
||||
.card-header { padding: 16px 16px 8px; border-bottom: 1px solid #f0f0f0; }
|
||||
.data-name { font-size: 16px; font-weight: 500; color: #1890ff; word-break: break-all; cursor: pointer; text-decoration: underline; }
|
||||
.time-info { text-align: right; font-size: 12px; color: #666; }
|
||||
.time-info .create-time, .time-info .update-time { margin-bottom: 4px; }
|
||||
.card-body { padding: 12px 16px 16px; }
|
||||
.data-description, .data-storage { margin-bottom: 8px; font-size: 14px; }
|
||||
.data-description .label, .data-storage .label { color: #666; font-weight: 500; }
|
||||
.card-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
|
||||
.card-footer { margin-top: auto; padding: 12px 16px; border-top: 1px solid rgba(0,0,0,0.08); background-color: rgba(255,255,255,0.8); border-bottom-left-radius: inherit; border-bottom-right-radius: inherit; }
|
||||
.favorite-btn { border-color: #1890ff; color: #1890ff; }
|
||||
.favorite-btn:hover { background-color: #e6f7ff; }
|
||||
.favorite-btn.ant-btn-disabled { color: #52c41a !important; border-color: #52c41a !important; background-color: rgba(82,196,26,0.1) !important; cursor: default !important; }
|
||||
.apply-btn { background-color: #1890ff; border-color: #1890ff; }
|
||||
.apply-btn:hover { background-color: #096dd9; }
|
||||
.loading-container { display: flex; justify-content: center; padding: 40px 0; }
|
||||
.no-data { text-align: center; padding: 40px 0; color: #666; }
|
||||
|
||||
/* 空白页内容区:居中展示,控制宽高比例 */
|
||||
.empty-content {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
@media (max-width: 576px) {
|
||||
.search-row { flex-wrap: wrap; gap: 8px; }
|
||||
.select-col, .input-col, .button-col { width: 100% !important; margin-bottom: 8px; }
|
||||
.button-col { margin-bottom: 0; }
|
||||
}
|
||||
|
||||
/* 空状态图标:突出显示,统一风格 */
|
||||
.empty-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background-color: #e8f4ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.empty-icon :deep(svg) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* 标题样式:与原项目标题层级一致 */
|
||||
.empty-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 描述文本:浅色、易读 */
|
||||
.empty-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 操作按钮区:间距控制 */
|
||||
.empty-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 辅助信息:弱化显示,底部对齐 */
|
||||
.empty-meta {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 响应式:小屏幕适配 */
|
||||
@media (max-width: 768px) {
|
||||
.empty-content {
|
||||
padding: 32px 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.empty-icon :deep(svg) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.empty-meta {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
162
capi-ui/src/views/biz/ssh/index.vue
Normal file
162
capi-ui/src/views/biz/ssh/index.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<!-- 根容器:100%填充父容器(home-content),确保自适应 -->
|
||||
<div class="empty-page-container">
|
||||
<!-- 空白页核心内容区:垂直居中 -->
|
||||
<div class="empty-content">
|
||||
<!-- 空状态图标 -->
|
||||
<div class="empty-icon">
|
||||
<FileTextOutlined />
|
||||
</div>
|
||||
|
||||
<!-- 标题与描述 -->
|
||||
<h2 class="empty-title">这是一个空白页面</h2>
|
||||
<p class="empty-desc">
|
||||
你可以在这里扩展功能,比如添加数据展示、表单提交、图表分析等内容<br>
|
||||
页面已适配父容器尺寸,支持高度/宽度自适应
|
||||
</p>
|
||||
|
||||
<!-- 操作按钮:提供基础交互入口 -->
|
||||
<div class="empty-actions">
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<PlusOutlined class="mr-2" /> 创建内容
|
||||
</a-button>
|
||||
<a-button @click="handleGuide" style="margin-left: 12px;">
|
||||
<QuestionCircleOutlined class="mr-2" /> 查看使用指南
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 辅助提示:展示页面基础信息 -->
|
||||
<div class="empty-meta">
|
||||
<span class="meta-item">页面路径:@/views/EmptyTemplatePage.vue</span>
|
||||
<span class="meta-item">适配场景:列表页、详情页、表单页等初始模板</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
// 引入必要组件(与原项目组件库一致)
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { FileTextOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
// 页面基础状态(可根据需求扩展)
|
||||
const pageName = ref('空白模板页面');
|
||||
|
||||
// 按钮交互事件(示例:可根据业务逻辑修改)
|
||||
const handleCreate = () => {
|
||||
console.log(`[${pageName.value}] 触发「创建内容」操作`);
|
||||
// 实际场景可扩展:打开创建弹窗、跳转表单页面等
|
||||
alert('可在此处实现「创建内容」的业务逻辑');
|
||||
};
|
||||
|
||||
const handleGuide = () => {
|
||||
console.log(`[${pageName.value}] 查看使用指南`);
|
||||
// 实际场景可扩展:打开帮助文档、显示引导弹窗等
|
||||
alert('使用指南:\n1. 此页面已适配父容器自适应尺寸\n2. 可在 empty-content 内添加业务组件\n3. 样式可参考原项目规范调整');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 根容器:100%填充父容器,确保自适应 */
|
||||
.empty-page-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 空白页内容区:居中展示,控制宽高比例 */
|
||||
.empty-content {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 空状态图标:突出显示,统一风格 */
|
||||
.empty-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background-color: #e8f4ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.empty-icon :deep(svg) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* 标题样式:与原项目标题层级一致 */
|
||||
.empty-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 描述文本:浅色、易读 */
|
||||
.empty-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 操作按钮区:间距控制 */
|
||||
.empty-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 辅助信息:弱化显示,底部对齐 */
|
||||
.empty-meta {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 响应式:小屏幕适配 */
|
||||
@media (max-width: 768px) {
|
||||
.empty-content {
|
||||
padding: 32px 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.empty-icon :deep(svg) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.empty-meta {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -39,6 +39,9 @@ import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
|
||||
import type { LoginParams, LoginResult } from '@/api/auth'
|
||||
import { apiLogin } from '@/api/auth'
|
||||
|
||||
import { useAuthStore } from '@/store/index' // 引入状态管理
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
@@ -55,20 +58,23 @@ const handleSubmit = async () => {
|
||||
if (loading.value) return;
|
||||
|
||||
loading.value = true;
|
||||
const authStore = useAuthStore(); // 提前获取store实例,避免多次调用
|
||||
try {
|
||||
// 更清晰的变量命名,API返回通常包含code、data、msg等字段
|
||||
const response: LoginResult = await apiLogin(form);
|
||||
// 检查请求是否成功(通常200为成功状态码)
|
||||
if (response.code === 200 && response.data) {
|
||||
localStorage.setItem('userInfo', JSON.stringify(response.data));
|
||||
const token = response.data.token;
|
||||
localStorage.setItem('token', token );
|
||||
localStorage.setItem('userInfo', response.data);
|
||||
authStore.setAuthInfo(token, response.data);
|
||||
message.success(response.msg || '登录成功');
|
||||
// 延迟跳转提升用户体验
|
||||
setTimeout(() => {
|
||||
router.replace('/dashboard');
|
||||
router.replace('/index/console');
|
||||
}, 800);
|
||||
return; // 成功后终止函数,避免执行后续错误提示
|
||||
}
|
||||
|
||||
// 处理业务错误(如账号密码错误)
|
||||
message.error(response.msg || '登录失败,请检查账号密码');
|
||||
} catch (error) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
<template>
|
||||
顶顶顶顶顶顶顶顶顶顶顶顶顶顶
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -14,12 +14,12 @@ public class AuthInterceptor implements HandlerInterceptor {
|
||||
public boolean preHandle(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Object handler) throws Exception {
|
||||
// HttpSession session = request.getSession();
|
||||
// ApiUser apiUser = (ApiUser) session.getAttribute("Authorization");
|
||||
// if (apiUser == null) {
|
||||
// response.sendRedirect(request.getContextPath() + "/login");
|
||||
// return false;
|
||||
// }
|
||||
HttpSession session = request.getSession();
|
||||
ApiUser apiUser = (ApiUser) session.getAttribute("userInfo");
|
||||
if (apiUser == null) {
|
||||
response.sendRedirect(request.getContextPath() + "/login");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package com.mini.capi.config;
|
||||
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
@@ -13,35 +19,51 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private final AuthInterceptor authInterceptor;
|
||||
|
||||
// @Override
|
||||
// public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// registry.addResourceHandler("/cApi/**")
|
||||
// .addResourceLocations("classpath:/static/")
|
||||
// .setCachePeriod(0);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// @Override
|
||||
// public void addViewControllers(ViewControllerRegistry registry) {
|
||||
// registry.addViewController("/cApi/**")
|
||||
// .setViewName("forward:/cApi/index.html");
|
||||
// }
|
||||
|
||||
@Override
|
||||
public void addViewControllers(ViewControllerRegistry registry) {
|
||||
registry.addViewController("/login").setViewName("forward:/index.html");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations("classpath:/static/")
|
||||
.resourceChain(true)
|
||||
.addResolver(new PathResourceResolver() {
|
||||
@Override
|
||||
protected Resource getResource(String resourcePath, Resource location) throws IOException {
|
||||
Resource requested = super.getResource(resourcePath, location);
|
||||
// 文件存在就返回,不存在返回 index.html
|
||||
return requested != null ? requested : new ClassPathResource("/static/index.html");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(authInterceptor)
|
||||
.addPathPatterns("/**") // 需要拦截的路径
|
||||
.excludePathPatterns( // 排除的路径
|
||||
.addPathPatterns("/**") // 拦截所有请求
|
||||
.excludePathPatterns(
|
||||
// ① 排除 Vue 静态资源(匹配 static 目录下所有 js/css/img 等)
|
||||
"/js/**",
|
||||
"/css/**",
|
||||
"/img/**",
|
||||
"/favicon.ico",
|
||||
"/index.html", // 排除首页(Vue 入口)
|
||||
"/assets/**", // 若 Vue 打包后有 assets 目录,需排除
|
||||
"/resource/**", // 若有其他静态资源目录,需排除
|
||||
"/cApi/index/**",
|
||||
|
||||
"/login",
|
||||
"/index.html",
|
||||
"/assets/**",
|
||||
"/resource/**",
|
||||
"/swagger-ui/**",
|
||||
"/v3/api-docs/**",
|
||||
"/Sys/jobs/**",
|
||||
"/Sys/login/**", // 你的登录接口(原 /Sys/login/** 缺少 /cApi 前缀,修复)
|
||||
"/Sys/jobs/**", // 原路径补充 /cApi 前缀
|
||||
"/Sys/hosts/**",
|
||||
"/Sys/dbs/**",
|
||||
"/Sys/login/**"
|
||||
|
||||
"/cApi/swagger-ui/**",
|
||||
"/cApi/v3/api-docs/**"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package com.mini.capi.job;
|
||||
|
||||
|
||||
import com.mini.capi.model.ApiResult;
|
||||
import com.mini.capi.sys.service.DbService;
|
||||
import com.mini.capi.sys.service.DockerService;
|
||||
import com.mini.capi.utils.*;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/Sys/jobs")
|
||||
public class taskEnable {
|
||||
|
||||
|
||||
@Resource
|
||||
private DockerService dockerService;
|
||||
|
||||
@Resource
|
||||
private DbService dbService;
|
||||
|
||||
|
||||
/**
|
||||
* 获取容器主机的磁盘使用情况
|
||||
*/
|
||||
@GetMapping("/getTaskDockerDiskInfo")
|
||||
public ApiResult<?> jobHostDisk(String token) {
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return dockerService.jobHostDisk();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 运行全部任务数据同步
|
||||
*/
|
||||
@GetMapping("/getTaskSyncDbInfo")
|
||||
public ApiResult<?> jobSyncAllTask(String token) {
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return dbService.jobSyncAllTask();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.mini.capi.model;
|
||||
|
||||
import com.mini.capi.biz.domain.SyncTablesView;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ApiResult<T> implements Serializable {
|
||||
@@ -37,6 +39,7 @@ public class ApiResult<T> implements Serializable {
|
||||
this.result = data;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------- 静态工厂方法 ---------------- */
|
||||
|
||||
/**
|
||||
|
||||
124
src/main/java/com/mini/capi/sys/Api/apiController.java
Normal file
124
src/main/java/com/mini/capi/sys/Api/apiController.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package com.mini.capi.sys.Api;
|
||||
|
||||
import com.mini.capi.model.ApiResult;
|
||||
import com.mini.capi.model.TabResult;
|
||||
import com.mini.capi.sys.service.DbService;
|
||||
import com.mini.capi.sys.service.DockerService;
|
||||
import com.mini.capi.sys.service.HostService;
|
||||
import com.mini.capi.utils.vToken;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/Sys/api")
|
||||
public class apiController {
|
||||
|
||||
|
||||
@Resource
|
||||
private DbService dbService;
|
||||
|
||||
@Resource
|
||||
private HostService hostService;
|
||||
|
||||
|
||||
@Resource
|
||||
private DockerService dockerService;
|
||||
|
||||
|
||||
/**
|
||||
* 获取MySQL的当前连接下的所有数据表
|
||||
*/
|
||||
@GetMapping("/getApiSourceTables")
|
||||
public ApiResult<List<TabResult>> listSourceTables(String token, String dbId) {
|
||||
// 1. 验证token有效性
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return dbService.listSourceTables(dbId);
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/getApiInfo")
|
||||
public ApiResult<List<HostService.SnapshotDTO>> getApiInfo(String token) {
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return hostService.getApiInfo();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取容器列表
|
||||
*/
|
||||
@GetMapping("/getApiDockerInfo")
|
||||
public ApiResult<?> getDockerInfo(String token) {
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return hostService.getDockerInfo();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 启动容器
|
||||
*/
|
||||
@GetMapping("/getApiStartDockerInfo")
|
||||
public ApiResult<?> startDockerInfo(String id, String token) {
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return hostService.startDockerInfo(id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 停止容器
|
||||
*/
|
||||
@GetMapping("/getApiStopDockerInfo")
|
||||
public ApiResult<?> stopDockerInfo(String id, String token) {
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return hostService.stopDockerInfo(id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取容器主机的磁盘使用情况
|
||||
*/
|
||||
@GetMapping("/getTaskDockerDiskInfo")
|
||||
public ApiResult<?> jobHostDisk(String token) {
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return dockerService.jobHostDisk();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 运行全部任务数据同步
|
||||
*/
|
||||
@GetMapping("/getTaskSyncDbInfo")
|
||||
public ApiResult<?> jobSyncAllTask(String token) {
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return dbService.jobSyncAllTask();
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行单个任务
|
||||
*/
|
||||
@GetMapping("/getTaskSyncDbByInfo")
|
||||
public ApiResult<?> jobSyncOneTask(String token, String taskId) {
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return dbService.jobSyncOneTask(taskId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +1,32 @@
|
||||
package com.mini.capi.sys.controller;
|
||||
package com.mini.capi.sys.App;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.mini.capi.biz.domain.DbConfig;
|
||||
import com.mini.capi.biz.domain.SyncTablesView;
|
||||
import com.mini.capi.biz.domain.SyncTask;
|
||||
import com.mini.capi.biz.service.DbConfigService;
|
||||
import com.mini.capi.biz.service.SyncTablesViewService;
|
||||
import com.mini.capi.biz.service.SyncTaskService;
|
||||
import com.mini.capi.config.DataSourceConfig;
|
||||
import com.mini.capi.model.ApiResult;
|
||||
import com.mini.capi.sys.domain.SelectOption;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.Data;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Controller
|
||||
@RestController
|
||||
@RequestMapping("/Sys/data")
|
||||
public class dataController {
|
||||
|
||||
|
||||
@@ -26,12 +35,11 @@ public class dataController {
|
||||
|
||||
|
||||
@Resource
|
||||
private DbConfigService dbConfigService;
|
||||
private DbConfigService configService;
|
||||
|
||||
@GetMapping("/Sys/data/list")
|
||||
public String listPage() {
|
||||
return "views/data/list";
|
||||
}
|
||||
|
||||
@Resource
|
||||
private SyncTablesViewService tablesViewService;
|
||||
|
||||
|
||||
@Data
|
||||
@@ -73,6 +81,28 @@ public class dataController {
|
||||
}
|
||||
|
||||
|
||||
@Data
|
||||
static class TableInfo implements Serializable {
|
||||
private TabDetail tabDetail;
|
||||
private List<TabColumns> tabColumns;
|
||||
private long pkCnt;
|
||||
private long idxCnt;
|
||||
private long colCnt;
|
||||
private String ddlSql;
|
||||
private String selectSql;
|
||||
|
||||
public TableInfo(TabDetail tabDetail, List<TabColumns> tabColumns, long pkCnt, long idxCnt, long colCnt, String ddlSql, String selectSql) {
|
||||
this.tabDetail = tabDetail;
|
||||
this.tabColumns = tabColumns;
|
||||
this.pkCnt = pkCnt;
|
||||
this.idxCnt = idxCnt;
|
||||
this.colCnt = colCnt;
|
||||
this.ddlSql = ddlSql;
|
||||
this.selectSql = selectSql;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static String buildSelect(List<TabColumns> columns, TabDetail table) {
|
||||
if (columns == null || columns.isEmpty() || table == null) {
|
||||
return "-- no columns or table info";
|
||||
@@ -160,18 +190,78 @@ public class dataController {
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/Sys/data/getTableDetail")
|
||||
public String getTableDetail(String taskId, Model model) {
|
||||
SyncTask task = syncTaskService.getById(taskId);
|
||||
DbConfig dbConfig = dbConfigService.getById(task.getSourceDbId());
|
||||
List<TabDetail> data;
|
||||
List<TabColumns> columns;
|
||||
/**
|
||||
* 获取数据配置表
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/getDbList")
|
||||
public ApiResult<List<SelectOption>> getDbList() {
|
||||
try {
|
||||
JdbcTemplate jdbcTemplate = DataSourceConfig.createJdbcTemplate(dbConfig);
|
||||
// 补充参数传递
|
||||
String querySql = "SELECT CREATE_TIME,TABLE_NAME,TABLE_COMMENT,MAX_DATA_LENGTH,UPDATE_TIME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?";
|
||||
List<DbConfig> configs = configService.list();
|
||||
List<SelectOption> selectOptions = configs.stream()
|
||||
.map(config -> new SelectOption(
|
||||
config.getDbId(), // 下拉框value(绑定值)
|
||||
config.getDbName(), // 下拉框label(显示文本)
|
||||
false // 【关键注释】说明禁用原因(如:所有数据库默认禁用选择,需申请后启用)
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
return ApiResult.success(selectOptions);
|
||||
} catch (Exception e) {
|
||||
return ApiResult.error(401, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据字段表
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/getTableList")
|
||||
public ApiResult<List<SyncTablesView>> getTableList(String dbId, String targetTable) {
|
||||
try {
|
||||
List<SyncTablesView> tablesViews = tablesViewService.list(
|
||||
new QueryWrapper<SyncTablesView>()
|
||||
.eq(StringUtils.hasText(dbId), "db_id", dbId)
|
||||
.and(StringUtils.hasText(targetTable),
|
||||
w -> w.like("target_table", targetTable)
|
||||
.or()
|
||||
.like("task_name", targetTable))
|
||||
.orderByDesc("create_time")
|
||||
);
|
||||
return ApiResult.success(tablesViews);
|
||||
} catch (Exception e) {
|
||||
return ApiResult.error(401, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据表属性
|
||||
*
|
||||
* @param taskId
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/Sys/data/getTableDetail")
|
||||
public ApiResult<TableInfo> getTableDetail(String taskId) {
|
||||
// 获取基础数据
|
||||
SyncTask task = syncTaskService.getById(taskId);
|
||||
DbConfig dbConfig = configService.getById(task.getSourceDbId());
|
||||
JdbcTemplate jdbcTemplate;
|
||||
try {
|
||||
// 统一创建JdbcTemplate,避免重复初始化
|
||||
jdbcTemplate = DataSourceConfig.createJdbcTemplate(dbConfig);
|
||||
} catch (Exception e) {
|
||||
return ApiResult.error(401, e.getMessage());
|
||||
}
|
||||
|
||||
// 查询表基本信息
|
||||
List<TabDetail> data;
|
||||
try {
|
||||
String tableQuerySql = "SELECT CREATE_TIME,TABLE_NAME,TABLE_COMMENT,MAX_DATA_LENGTH,UPDATE_TIME " +
|
||||
"FROM information_schema.TABLES " +
|
||||
"WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?";
|
||||
data = jdbcTemplate.query(
|
||||
querySql,
|
||||
tableQuerySql,
|
||||
(rs, rowNum) -> new TabDetail(
|
||||
rs.getString("CREATE_TIME"),
|
||||
dbConfig.getDbName(),
|
||||
@@ -185,16 +275,17 @@ public class dataController {
|
||||
);
|
||||
data.sort(Comparator.comparing(TabDetail::getTableName));
|
||||
} catch (Exception e) {
|
||||
data = Collections.emptyList();
|
||||
System.out.println(e.getMessage());
|
||||
}
|
||||
|
||||
return ApiResult.error(401, e.getMessage());
|
||||
}
|
||||
// 查询表列信息
|
||||
List<TabColumns> columns;
|
||||
try {
|
||||
JdbcTemplate jdbcTemplate = DataSourceConfig.createJdbcTemplate(dbConfig);
|
||||
// 补充参数传递
|
||||
String querySql = "SELECT TABLE_NAME,ORDINAL_POSITION,COLUMN_NAME,COLUMN_TYPE,COLUMN_COMMENT,COLUMN_KEY FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?";
|
||||
String columnQuerySql = "SELECT TABLE_NAME,ORDINAL_POSITION,COLUMN_NAME,COLUMN_TYPE,COLUMN_COMMENT,COLUMN_KEY " +
|
||||
"FROM information_schema.COLUMNS " +
|
||||
"WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?";
|
||||
columns = jdbcTemplate.query(
|
||||
querySql,
|
||||
columnQuerySql,
|
||||
(rs, rowNum) -> new TabColumns(
|
||||
rs.getString("TABLE_NAME"),
|
||||
rs.getInt("ORDINAL_POSITION"),
|
||||
@@ -208,26 +299,29 @@ public class dataController {
|
||||
);
|
||||
columns.sort(Comparator.comparing(TabColumns::getSort));
|
||||
} catch (Exception e) {
|
||||
columns = Collections.emptyList();
|
||||
System.out.println(e.getMessage());
|
||||
return ApiResult.error(401, e.getMessage());
|
||||
}
|
||||
// 在查询完表结构后
|
||||
|
||||
// 计算表结构统计信息
|
||||
long primaryKeyCnt = columns.stream()
|
||||
.filter(c -> "PRI".equalsIgnoreCase(c.getKeyType()))
|
||||
.count();
|
||||
long indexCnt = columns.stream()
|
||||
.filter(c -> !"PRI".equalsIgnoreCase(c.getKeyType()) && !c.getKeyType().isBlank())
|
||||
.count()
|
||||
+ 1; // 主键索引也算 1 个
|
||||
.count() + 1; // 包含主键索引
|
||||
long colCnt = columns.size();
|
||||
model.addAttribute("pkCnt", primaryKeyCnt);
|
||||
model.addAttribute("idxCnt", indexCnt);
|
||||
model.addAttribute("colCnt", colCnt);
|
||||
model.addAttribute("data", data);
|
||||
model.addAttribute("columns", columns);
|
||||
model.addAttribute("ddlSql", buildDDL(columns, data.get(0)));
|
||||
model.addAttribute("selectSql", buildSelect(columns, data.get(0)));
|
||||
return "views/data/detail";
|
||||
|
||||
// 构建返回结果
|
||||
TableInfo tableInfo = new TableInfo(
|
||||
data.get(0),
|
||||
columns,
|
||||
primaryKeyCnt,
|
||||
indexCnt,
|
||||
colCnt,
|
||||
buildDDL(columns, data.get(0)),
|
||||
buildSelect(columns, data.get(0))
|
||||
);
|
||||
return ApiResult.success(tableInfo);
|
||||
}
|
||||
|
||||
|
||||
9
src/main/java/com/mini/capi/sys/App/hostController.java
Normal file
9
src/main/java/com/mini/capi/sys/App/hostController.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package com.mini.capi.sys.App;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/Sys/hosts")
|
||||
public class hostController {
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.mini.capi.sys.pageController;
|
||||
package com.mini.capi.sys.App;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.mini.capi.biz.domain.ApiMenus;
|
||||
@@ -10,6 +10,7 @@ import com.mini.capi.biz.service.ApiUserService;
|
||||
import com.mini.capi.model.ApiResult;
|
||||
import com.mini.capi.model.auth.Result;
|
||||
import com.mini.capi.sys.domain.ModuleMenuDTO;
|
||||
import com.mini.capi.utils.KeyUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lombok.Data;
|
||||
@@ -20,10 +21,9 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/Sys/login")
|
||||
public class loginPageController {
|
||||
public class loginController {
|
||||
|
||||
|
||||
@Resource
|
||||
@@ -58,11 +58,14 @@ public class loginPageController {
|
||||
*/
|
||||
private String uname;
|
||||
|
||||
private String token;
|
||||
|
||||
// 构造方法(从实体类转换)
|
||||
public ApiUserDTO(ApiUser apiUser) {
|
||||
public ApiUserDTO(ApiUser apiUser, String token) {
|
||||
this.userId = apiUser.getUserId();
|
||||
this.username = apiUser.getApiUser();
|
||||
this.uname = apiUser.getUname();
|
||||
this.token = token;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,14 +94,25 @@ public class loginPageController {
|
||||
// 可记录登录失败日志,用于后续风控
|
||||
return Result.error(102, "账户或密码错误");
|
||||
}
|
||||
session.setAttribute("token", apiUser);
|
||||
ApiUserDTO userDTO = new ApiUserDTO(apiUser);
|
||||
session.setAttribute("userInfo", apiUser);
|
||||
ApiUserDTO userDTO = new ApiUserDTO(apiUser, KeyUtil.ObjKey(32, 0));
|
||||
return Result.success("登录成功", userDTO);
|
||||
} catch (Exception e) {
|
||||
return Result.error(103, "登录失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/userLogout")
|
||||
public Result loginOut(HttpSession session) {
|
||||
try {
|
||||
session.removeAttribute("userInfo"); // 只清 token
|
||||
session.invalidate();
|
||||
return Result.error(200, "退出成功");
|
||||
} catch (Exception e) {
|
||||
return Result.error(500, "退出失败");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/getModules")
|
||||
public ApiResult<List<ModuleMenuDTO>> getModules() {
|
||||
@@ -1,31 +1,40 @@
|
||||
package com.mini.capi.sys.controller;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest; // 注意这里
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.boot.web.servlet.error.ErrorController;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
public class CustomErrorController implements ErrorController {
|
||||
|
||||
@RequestMapping("/error")
|
||||
public ResponseEntity<Map<String, Object>> handleError(HttpServletRequest request) {
|
||||
public Object handleError(HttpServletRequest request) {
|
||||
String uri = request.getRequestURI();
|
||||
|
||||
// 1. 前端路由:统一转发到 index.html
|
||||
if (uri.startsWith("/cApi/index/")) {
|
||||
return "forward:/index.html";
|
||||
}
|
||||
|
||||
// 2. 其他 404:返回 JSON
|
||||
HttpStatus status = getStatus(request);
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("status", status.value());
|
||||
body.put("error", status.getReasonPhrase());
|
||||
body.put("message", "访问的资源不存在");
|
||||
body.put("path", request.getRequestURI());
|
||||
return ResponseEntity.status(status).body(body);
|
||||
return ResponseEntity
|
||||
.status(status)
|
||||
.body(Map.of(
|
||||
"status", status.value(),
|
||||
"error", status.getReasonPhrase(),
|
||||
"message", "访问的资源不存在",
|
||||
"path", uri
|
||||
));
|
||||
}
|
||||
|
||||
private HttpStatus getStatus(HttpServletRequest request) {
|
||||
Integer code = (Integer) request.getAttribute("jakarta.servlet.error.status_code");
|
||||
return (code != null) ? HttpStatus.valueOf(code) : HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
return code != null ? HttpStatus.valueOf(code) : HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.mini.capi.sys.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class hostController {
|
||||
|
||||
|
||||
/**
|
||||
* 主机登录
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/Sys/app/host")
|
||||
public String listPage() {
|
||||
return "views/ssh/index";
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.mini.capi.sys.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class loginController {
|
||||
|
||||
@GetMapping("/login")
|
||||
public String loginPage() {
|
||||
return "forward:/index.html";
|
||||
}
|
||||
|
||||
//
|
||||
// /**
|
||||
// * 退出登录:清空 session 并返回到退出成功页面
|
||||
// */
|
||||
// @GetMapping("/userLogout")
|
||||
// public String logout(HttpSession session) {
|
||||
// session.invalidate();
|
||||
// return "index";
|
||||
// }
|
||||
//
|
||||
//
|
||||
// /**
|
||||
// * 主页
|
||||
// */
|
||||
// @GetMapping("/welcome")
|
||||
// public String welcomePage() {
|
||||
// return "views/demo";
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 系统首页-控制台
|
||||
// */
|
||||
// @GetMapping("/home")
|
||||
// public String homePage() {
|
||||
// return "views/home";
|
||||
// }
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.mini.capi.sys.controller;
|
||||
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class sysController {
|
||||
|
||||
|
||||
@GetMapping("/Sys/sys/icon")
|
||||
public String getIcon() {
|
||||
return "views/icon";
|
||||
}
|
||||
}
|
||||
28
src/main/java/com/mini/capi/sys/domain/SelectOption.java
Normal file
28
src/main/java/com/mini/capi/sys/domain/SelectOption.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.mini.capi.sys.domain;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
public class SelectOption implements Serializable {
|
||||
|
||||
|
||||
private String value;
|
||||
|
||||
|
||||
private String label;
|
||||
|
||||
private boolean disabled;
|
||||
|
||||
public SelectOption() {
|
||||
}
|
||||
|
||||
// 全参构造函数
|
||||
public SelectOption(String value, String label, boolean disabled) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
this.disabled = disabled;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.mini.capi.sys.pageController;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.mini.capi.biz.domain.DbConfig;
|
||||
import com.mini.capi.biz.domain.SyncTablesView;
|
||||
import com.mini.capi.biz.service.DbConfigService;
|
||||
import com.mini.capi.biz.service.SyncTablesViewService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController // ← 关键
|
||||
@RequestMapping("/Sys/data")
|
||||
public class dataPageController {
|
||||
|
||||
|
||||
@Resource
|
||||
private DbConfigService configService;
|
||||
|
||||
|
||||
@Resource
|
||||
private SyncTablesViewService tablesViewService;
|
||||
|
||||
|
||||
@GetMapping("/getDbList")
|
||||
public List<DbConfig> getDbList() {
|
||||
return configService.list();
|
||||
}
|
||||
|
||||
@GetMapping("/getTableList")
|
||||
public List<SyncTablesView> getTableList(String dbId, String targetTable) {
|
||||
return tablesViewService.list(
|
||||
new QueryWrapper<SyncTablesView>()
|
||||
.eq(StringUtils.hasText(dbId), "db_id", dbId)
|
||||
.and(StringUtils.hasText(targetTable),
|
||||
w -> w.like("target_table", targetTable)
|
||||
.or()
|
||||
.like("task_name", targetTable))
|
||||
.orderByDesc("create_time")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package com.mini.capi.sys.pageController;
|
||||
|
||||
import com.mini.capi.model.ApiResult;
|
||||
import com.mini.capi.model.TabResult;
|
||||
import com.mini.capi.sys.service.DbService;
|
||||
import com.mini.capi.utils.vToken;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/Sys/dbs")
|
||||
public class dbPageController {
|
||||
|
||||
|
||||
@Resource
|
||||
private DbService dbService;
|
||||
|
||||
/**
|
||||
* 获取MySQL的当前连接下的所有数据表
|
||||
*/
|
||||
@GetMapping("/getApiSourceTables")
|
||||
public ApiResult<List<TabResult>> listSourceTables(String token, String dbId) {
|
||||
// 1. 验证token有效性
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return dbService.listSourceTables(dbId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 运行单个任务
|
||||
*/
|
||||
@GetMapping("/getTaskSyncDbByInfo")
|
||||
public ApiResult<?> jobSyncOneTask(String token, String taskId) {
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return dbService.jobSyncOneTask(taskId);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.mini.capi.sys.pageController;
|
||||
|
||||
|
||||
import com.mini.capi.model.ApiResult;
|
||||
import com.mini.capi.sys.service.HostService;
|
||||
import com.mini.capi.utils.vToken;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/Sys/hosts")
|
||||
public class hostPageController {
|
||||
|
||||
|
||||
@Resource
|
||||
private HostService hostService;
|
||||
|
||||
|
||||
@GetMapping("/getApiInfo")
|
||||
public ApiResult<List<HostService.SnapshotDTO>> getApiInfo(String token) {
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return hostService.getApiInfo();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取容器列表
|
||||
*/
|
||||
@GetMapping("/getApiDockerInfo")
|
||||
public ApiResult<?> getDockerInfo(String token) {
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return hostService.getDockerInfo();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 启动容器
|
||||
*/
|
||||
@GetMapping("/getApiStartDockerInfo")
|
||||
public ApiResult<?> startDockerInfo(String id, String token) {
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return hostService.startDockerInfo(id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 停止容器
|
||||
*/
|
||||
@GetMapping("/getApiStopDockerInfo")
|
||||
public ApiResult<?> stopDockerInfo(String id, String token) {
|
||||
if (vToken.isValidToken(token)) {
|
||||
return ApiResult.error(401, "无效的访问令牌");
|
||||
}
|
||||
return hostService.stopDockerInfo(id);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
81
src/main/java/com/mini/capi/utils/AESUtil.java
Normal file
81
src/main/java/com/mini/capi/utils/AESUtil.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package com.mini.capi.utils;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.apache.commons.net.util.Base64;
|
||||
|
||||
public class AESUtil {
|
||||
public static String key = "AD42F6697B035B7580E4FEF93BE20BAD";
|
||||
private static String charset = "utf-8";
|
||||
// 偏移量
|
||||
private static int offset = 16;
|
||||
private static String transformation = "AES/CBC/PKCS5Padding";
|
||||
private static String algorithm = "AES";
|
||||
|
||||
/**
|
||||
* 加密
|
||||
*
|
||||
* @param content
|
||||
* @return
|
||||
*/
|
||||
public static String encrypt(String content) {
|
||||
return encrypt(content, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密
|
||||
*
|
||||
* @param content
|
||||
* @return
|
||||
*/
|
||||
public static String decrypt(String content) {
|
||||
return decrypt(content, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密
|
||||
*
|
||||
* @param content 需要加密的内容
|
||||
* @param key 加密密码
|
||||
* @return
|
||||
*/
|
||||
public static String encrypt(String content, String key) {
|
||||
try {
|
||||
SecretKeySpec skey = new SecretKeySpec(key.getBytes(), algorithm);
|
||||
IvParameterSpec iv = new IvParameterSpec(key.getBytes(), 0, offset);
|
||||
Cipher cipher = Cipher.getInstance(transformation);
|
||||
byte[] byteContent = content.getBytes(charset);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, skey, iv);// 初始化
|
||||
byte[] result = cipher.doFinal(byteContent);
|
||||
return new Base64().encodeToString(result).replaceAll("[\\s*\t\n\r]", ""); // 加密
|
||||
} catch (Exception e) {
|
||||
System.out.println(e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* AES(256)解密
|
||||
*
|
||||
* @param content 待解密内容
|
||||
* @param key 解密密钥
|
||||
* @return 解密之后
|
||||
* @throws Exception
|
||||
*/
|
||||
public static String decrypt(String content, String key) {
|
||||
try {
|
||||
|
||||
SecretKeySpec skey = new SecretKeySpec(key.getBytes(), algorithm);
|
||||
IvParameterSpec iv = new IvParameterSpec(key.getBytes(charset), 0, offset);
|
||||
Cipher cipher = Cipher.getInstance(transformation);
|
||||
cipher.init(Cipher.DECRYPT_MODE, skey, iv);// 初始化
|
||||
byte[] result = cipher.doFinal(new Base64().decode(content));
|
||||
return new String(result).replaceAll("[\\s*\t\n\r]", ""); // 解密
|
||||
} catch (Exception e) {
|
||||
System.out.println(e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
48
src/main/java/com/mini/capi/utils/KeyUtil.java
Normal file
48
src/main/java/com/mini/capi/utils/KeyUtil.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.mini.capi.utils;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public class KeyUtil {
|
||||
|
||||
public static String ObjKey(int length, int type) {
|
||||
Random random = new Random();
|
||||
StringBuffer key = new StringBuffer();
|
||||
if (type == 1) {
|
||||
String str = "0123456789";
|
||||
for (int i = 0; i < length; ++i) {
|
||||
//从62个的数字或字母中选择
|
||||
int number = random.nextInt(10);
|
||||
//将产生的数字通过length次承载到key中
|
||||
key.append(str.charAt(number));
|
||||
}
|
||||
return key.toString();
|
||||
} else if (type == 2) {
|
||||
String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
for (int i = 0; i < length; ++i) {
|
||||
//从62个的数字或字母中选择
|
||||
int number = random.nextInt(36);
|
||||
//将产生的数字通过length次承载到key中
|
||||
key.append(str.charAt(number));
|
||||
}
|
||||
return key.toString();
|
||||
} else if (type == 3) {
|
||||
String str = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
for (int i = 0; i < length; ++i) {
|
||||
//从62个的数字或字母中选择
|
||||
int number = random.nextInt(36);
|
||||
//将产生的数字通过length次承载到key中
|
||||
key.append(str.charAt(number));
|
||||
}
|
||||
return key.toString();
|
||||
} else {
|
||||
String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
for (int i = 0; i < length; ++i) {
|
||||
//从62个的数字或字母中选择
|
||||
int number = random.nextInt(62);
|
||||
//将产生的数字通过length次承载到key中
|
||||
key.append(str.charAt(number));
|
||||
}
|
||||
return key.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,4 +30,5 @@ webssh.file.upload-max-size=100MB
|
||||
webssh.file.temp-dir=/ogsapp/temp/webssh-uploads
|
||||
webssh.collaboration.enabled=true
|
||||
webssh.collaboration.max-participants=10
|
||||
webssh.collaboration.session-timeout=3600000
|
||||
webssh.collaboration.session-timeout=3600000
|
||||
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
|
||||
@@ -1 +0,0 @@
|
||||
const n={};n.render=function(n,e){return null};export{n as default};
|
||||
@@ -1 +0,0 @@
|
||||
@charset "UTF-8";.login-wrapper[data-v-c93922d0]{height:100vh;display:flex;align-items:center;justify-content:flex-end;padding-right:200px;background:linear-gradient(120deg,rgba(79,172,254,.7) 0%,rgba(0,242,254,.7) 50%,rgba(122,90,248,.7) 100%),url(/cApi/assets/backImg.5cfc718b.jpg) center center no-repeat;background-blend-mode:overlay;background-size:cover}.login-text[data-v-c93922d0]{text-align:center;color:#fff;width:800px;padding:32px 48px;border-radius:8px}.login-card[data-v-c93922d0]{width:520px;padding:32px 48px;border-radius:8px;box-shadow:0 8px 24px #0000001f}.login-header[data-v-c93922d0]{text-align:center;margin-bottom:32px}.login-header .logo[data-v-c93922d0]{width:64px;height:64px;margin-bottom:12px}.login-header h1[data-v-c93922d0]{font-size:24px;font-weight:600;margin:0}.login-header p[data-v-c93922d0]{font-size:14px;color:#8c8c8c;margin:8px 0 0}
|
||||
1
src/main/resources/static/assets/Login.2dfba5c8.css
Normal file
1
src/main/resources/static/assets/Login.2dfba5c8.css
Normal file
@@ -0,0 +1 @@
|
||||
@charset "UTF-8";.login-wrapper[data-v-c077d3e4]{height:100vh;display:flex;align-items:center;justify-content:flex-end;padding-right:200px;background:linear-gradient(120deg,rgba(79,172,254,.7) 0%,rgba(0,242,254,.7) 50%,rgba(122,90,248,.7) 100%),url(/cApi/assets/backImg.5cfc718b.jpg) center center no-repeat;background-blend-mode:overlay;background-size:cover}.login-text[data-v-c077d3e4]{text-align:center;color:#fff;width:800px;padding:32px 48px;border-radius:8px}.login-card[data-v-c077d3e4]{width:520px;padding:32px 48px;border-radius:8px;box-shadow:0 8px 24px #0000001f}.login-header[data-v-c077d3e4]{text-align:center;margin-bottom:32px}.login-header .logo[data-v-c077d3e4]{width:64px;height:64px;margin-bottom:12px}.login-header h1[data-v-c077d3e4]{font-size:24px;font-weight:600;margin:0}.login-header p[data-v-c077d3e4]{font-size:14px;color:#8c8c8c;margin:8px 0 0}
|
||||
File diff suppressed because one or more lines are too long
1
src/main/resources/static/assets/Login.b6228c81.js
Normal file
1
src/main/resources/static/assets/Login.b6228c81.js
Normal file
@@ -0,0 +1 @@
|
||||
import{d as S,_ as E,a as h,u as D,r as f,b as k,o as w,c as y,e as u,w as o,f as s,g as c,h as _,U as x,L as b,i as L,m}from"./index.04365c99.js";import{a as N}from"./auth.545ce83f.js";var O="/cApi/assets/logo.03d6d6da.png";const U=S("auth",{state:()=>({token:localStorage.getItem("token")||null,userInfo:localStorage.getItem("userInfo")?JSON.parse(localStorage.getItem("userInfo")):null}),actions:{setAuthInfo(i,r){this.token=i,this.userInfo=r,localStorage.setItem("token",i),localStorage.setItem("userInfo",JSON.stringify(r))},clearAuthInfo(){this.token=null,this.userInfo=null,localStorage.removeItem("token"),localStorage.removeItem("userInfo")}}});const J={class:"login-wrapper"},R=h({__name:"Login",setup(i){const r=D(),g=f(),n=f(!1),t=k({account:"",password:""}),F={account:[{required:!0,message:"\u8BF7\u8F93\u5165\u7528\u6237\u540D"}],password:[{required:!0,message:"\u8BF7\u8F93\u5165\u5BC6\u7801"}]},B=async()=>{if(n.value)return;n.value=!0;const p=U();try{const e=await N(t);if(e.code===200&&e.data){const l=e.data.token,a=JSON.stringify(e.data);localStorage.setItem("token",l),localStorage.setItem("userInfo",a),p.setAuthInfo(l,a),m.success(e.msg||"\u767B\u5F55\u6210\u529F"),setTimeout(()=>{r.replace("/index/console")},800);return}m.error(e.msg||"\u767B\u5F55\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u8D26\u53F7\u5BC6\u7801")}catch(e){console.error("\u767B\u5F55\u8BF7\u6C42\u5F02\u5E38:",e),m.error("\u7F51\u7EDC\u5F02\u5E38\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u540E\u91CD\u8BD5")}finally{n.value=!1}};return(p,e)=>{const l=s("a-input"),a=s("a-form-item"),v=s("a-input-password"),I=s("a-button"),A=s("a-form"),C=s("a-card");return w(),y("div",J,[u(C,{class:"login-card",bordered:!1},{default:o(()=>[e[3]||(e[3]=c("div",{class:"login-header"},[c("img",{class:"logo",src:O,alt:"logo"}),c("h1",null,"cApi\u7BA1\u7406\u7CFB\u7EDF"),c("p",null,"\u5B89\u5168\u3001\u9AD8\u6548\u7684\u4E00\u7AD9\u5F0F\u7BA1\u7406\u89E3\u51B3\u65B9\u6848\uFF0C\u4E3A\u60A8\u7684\u4E1A\u52A1\u4FDD\u9A7E\u62A4\u822A")],-1)),u(A,{ref_key:"formRef",ref:g,model:t,rules:F,size:"large",onFinish:B},{default:o(()=>[u(a,{name:"account"},{default:o(()=>[u(l,{value:t.account,"onUpdate:value":e[0]||(e[0]=d=>t.account=d),placeholder:"\u8BF7\u8F93\u5165\u7528\u6237\u540D","allow-clear":""},{prefix:o(()=>[u(_(x))]),_:1},8,["value"])]),_:1}),u(a,{name:"password"},{default:o(()=>[u(v,{value:t.password,"onUpdate:value":e[1]||(e[1]=d=>t.password=d),placeholder:"\u8BF7\u8F93\u5165\u5BC6\u7801","allow-clear":""},{prefix:o(()=>[u(_(b))]),_:1},8,["value"])]),_:1}),u(a,null,{default:o(()=>[u(I,{type:"primary","html-type":"submit",loading:n.value,block:""},{default:o(()=>[...e[2]||(e[2]=[L(" \u767B\u5F55 ",-1)])]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"])]),_:1})])}}});var T=E(R,[["__scopeId","data-v-c077d3e4"]]);export{T as default};
|
||||
1
src/main/resources/static/assets/Main.44989b63.js
Normal file
1
src/main/resources/static/assets/Main.44989b63.js
Normal file
@@ -0,0 +1 @@
|
||||
import{_ as r}from"./index.04365c99.js";const e={};function j(_,c){return" jjjjjjjjj "}var t=r(e,[["render",j]]);export{t as default};
|
||||
1
src/main/resources/static/assets/NotFound.4eaa0d77.js
Normal file
1
src/main/resources/static/assets/NotFound.4eaa0d77.js
Normal file
@@ -0,0 +1 @@
|
||||
import{_ as r}from"./index.04365c99.js";const e={};function t(_,c){return" \u9876\u9876\u9876\u9876\u9876\u9876\u9876\u9876\u9876\u9876\u9876\u9876\u9876\u9876 "}var o=r(e,[["render",t]]);export{o as default};
|
||||
6
src/main/resources/static/assets/auth.545ce83f.js
Normal file
6
src/main/resources/static/assets/auth.545ce83f.js
Normal file
File diff suppressed because one or more lines are too long
1
src/main/resources/static/assets/icon.704eed01.css
Normal file
1
src/main/resources/static/assets/icon.704eed01.css
Normal file
@@ -0,0 +1 @@
|
||||
.icon-gallery[data-v-64d10c62]{width:100%;box-sizing:border-box}.search-input[data-v-64d10c62]{max-width:400px}.icons-grid[data-v-64d10c62]{width:100%;box-sizing:border-box}.icon-item[data-v-64d10c62]:hover{border-color:#1890ff;box-shadow:0 2px 8px #00000014}.icon-name[data-v-64d10c62]{font-size:12px;color:#595959;text-align:center;word-break:break-all;max-width:100%}.copy-buttons[data-v-64d10c62]{display:flex;gap:8px;margin-top:8px;width:100%;justify-content:center}.copy-btn[data-v-64d10c62]{padding:2px 8px!important;font-size:12px!important}.empty-state[data-v-64d10c62]{padding:40px 0;text-align:center}.copy-message[data-v-64d10c62]{position:fixed;bottom:24px;left:50%;transform:translate(-50%);z-index:1000}
|
||||
1
src/main/resources/static/assets/icon.a450540c.js
Normal file
1
src/main/resources/static/assets/icon.a450540c.js
Normal file
@@ -0,0 +1 @@
|
||||
import{_ as U,a as W,I as q,r as a,n as u,o as s,c as l,j as g,t as p,g as w,F as G,y as H,A as m,e as r,f as y,z as J,v as K,w as S,h as F,D as T,i as z,k as B}from"./index.04365c99.js";const Q=["onMouseenter"],X={class:"icon-name"},Y={key:0,class:"copy-buttons"},Z={key:1,class:"empty-state"},ee=W({__name:"icon",props:{showSearch:{type:Boolean,default:!0},searchSize:{type:String,default:"middle"},columns:{type:Number,default:6},iconSize:{type:Number,default:24},itemPadding:{type:String,default:"16px"},messageDuration:{type:Number,default:2e3}},setup(c){const n=c,N=["createFromIconfontCN","getTwoToneColor","setTwoToneColor","default","CopyOutlined"],_=Object.entries(q).filter(([e])=>!N.includes(e)).map(([e,t])=>({name:e,component:t})),i=a(""),C=u(()=>i.value?_.filter(({name:e})=>e.toLowerCase().includes(i.value.toLowerCase())):_),f=a(""),v=a(!1),h=a(""),D=a("success");function M(e){f.value=e}function $(){f.value=""}function E(e){const t=`<${e} />`;x(t).then(()=>{d(`\u5DF2\u590D\u5236: ${t}`,"success")}).catch(()=>{d("\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u590D\u5236","error")})}function I(e){x(e).then(()=>{d(`\u5DF2\u590D\u5236: ${e}`,"success")}).catch(()=>{d("\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u590D\u5236","error")})}async function x(e){try{return await navigator.clipboard.writeText(e),!0}catch(t){return console.error("\u65E0\u6CD5\u590D\u5236\u6587\u672C: ",t),!1}}function d(e,t){h.value=e,D.value=t,v.value=!0,setTimeout(()=>{v.value=!1},n.messageDuration)}const L=u(()=>({padding:"16px",backgroundColor:"#f5f5f5",borderRadius:"4px"})),A=u(()=>({display:"grid",gridTemplateColumns:`repeat(${n.columns}, 1fr)`,gap:"12px",marginTop:n.showSearch?"16px":0})),V=u(()=>({display:"flex",flexDirection:"column",alignItems:"center",backgroundColor:"#fff",border:"1px solid #e8e8e8",borderRadius:"6px",padding:n.itemPadding,cursor:"pointer",transition:"all 0.2s"})),O=u(()=>({fontSize:`${n.iconSize}px`,color:"#262626",marginBottom:"8px"}));return(e,t)=>{const j=y("a-input"),b=y("a-button"),P=y("a-empty"),R=y("a-message");return s(),l("div",{class:"icon-gallery",style:m(L.value)},[c.showSearch?(s(),g(j,{key:0,value:i.value,"onUpdate:value":t[0]||(t[0]=o=>i.value=o),placeholder:"\u641C\u7D22\u56FE\u6807\u540D\u79F0...","allow-clear":"",class:"search-input",size:c.searchSize},null,8,["value","size"])):p("",!0),w("div",{class:"icons-grid",style:m(A.value)},[(s(!0),l(G,null,H(C.value,o=>(s(),l("div",{key:o.name,class:"icon-item",style:m(V.value),onMouseenter:k=>M(o.name),onMouseleave:$},[(s(),g(J(o.component),{class:"icon",style:m(O.value)},null,8,["style"])),w("span",X,K(o.name),1),f.value===o.name?(s(),l("div",Y,[r(b,{type:"primary",size:"small",onClick:B(k=>E(o.name),["stop"]),class:"copy-btn"},{default:S(()=>[r(F(T),{class:"mr-1"}),t[1]||(t[1]=z("\u6807\u7B7E ",-1))]),_:1},8,["onClick"]),r(b,{type:"default",size:"small",onClick:B(k=>I(o.name),["stop"]),class:"copy-btn"},{default:S(()=>[r(F(T),{class:"mr-1"}),t[2]||(t[2]=z("\u540D\u79F0 ",-1))]),_:1},8,["onClick"])])):p("",!0)],44,Q))),128))],4),C.value.length===0?(s(),l("div",Z,[r(P,{description:"\u6CA1\u6709\u627E\u5230\u5339\u914D\u7684\u56FE\u6807"})])):p("",!0),v.value?(s(),g(R,{key:2,content:h.value,type:D.value,duration:c.messageDuration,class:"copy-message"},null,8,["content","type","duration"])):p("",!0)],4)}}});var oe=U(ee,[["__scopeId","data-v-64d10c62"]]);export{oe as default};
|
||||
1
src/main/resources/static/assets/index.02dafd63.js
Normal file
1
src/main/resources/static/assets/index.02dafd63.js
Normal file
@@ -0,0 +1 @@
|
||||
import{_ as r}from"./index.04365c99.js";const e={};function _(c,n){return" app "}var a=r(e,[["render",_]]);export{a as default};
|
||||
574
src/main/resources/static/assets/index.04365c99.js
Normal file
574
src/main/resources/static/assets/index.04365c99.js
Normal file
File diff suppressed because one or more lines are too long
1
src/main/resources/static/assets/index.19f796de.css
Normal file
1
src/main/resources/static/assets/index.19f796de.css
Normal file
File diff suppressed because one or more lines are too long
1
src/main/resources/static/assets/index.531b96f1.js
Normal file
1
src/main/resources/static/assets/index.531b96f1.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
html,body{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}@-ms-viewport{width:device-width}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[title],abbr[data-original-title]{-webkit-text-decoration:underline dotted;text-decoration:underline;text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=text],input[type=password],input[type=number],textarea{-webkit-appearance:none}ol,ul,dl{margin-top:0;margin-bottom:1em}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}pre,code,kbd,samp{font-size:1em;font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}a,area,button,[role=button],input:not([type="range"]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;text-align:left;caption-side:bottom}input,button,select,optgroup,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{padding:0;border-style:none}input[type=radio],input[type=checkbox]{box-sizing:border-box;padding:0}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}
|
||||
1
src/main/resources/static/assets/index.737965c1.css
Normal file
1
src/main/resources/static/assets/index.737965c1.css
Normal file
@@ -0,0 +1 @@
|
||||
.empty-page-container[data-v-2de931e9]{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#fff;border-radius:8px;box-shadow:0 1px 2px #0000000d}.empty-content[data-v-2de931e9]{width:100%;max-width:600px;padding:48px 24px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:20px}.empty-icon[data-v-2de931e9]{width:80px;height:80px;border-radius:50%;background-color:#e8f4ff;display:flex;align-items:center;justify-content:center;color:#1890ff}.empty-icon[data-v-2de931e9] svg{width:40px;height:40px}.empty-title[data-v-2de931e9]{font-size:18px;font-weight:500;color:#333;margin:0}.empty-desc[data-v-2de931e9]{font-size:14px;color:#666;line-height:1.6;margin:0}.empty-actions[data-v-2de931e9]{display:flex;gap:12px;margin-top:8px}.empty-meta[data-v-2de931e9]{margin-top:16px;display:flex;flex-wrap:wrap;justify-content:center;gap:16px;font-size:12px;color:#999}@media (max-width: 768px){.empty-content[data-v-2de931e9]{padding:32px 16px;gap:16px}.empty-icon[data-v-2de931e9]{width:64px;height:64px}.empty-icon[data-v-2de931e9] svg{width:32px;height:32px}.empty-title[data-v-2de931e9]{font-size:16px}.empty-meta[data-v-2de931e9]{flex-direction:column;gap:8px}}
|
||||
@@ -1 +0,0 @@
|
||||
import{r as e,o as r,c as t,a as o,b as n,d as s,A as i}from"./vendor.a8167e4a.js";!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver((e=>{for(const t of e)if("childList"===t.type)for(const e of t.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)})).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerpolicy&&(r.referrerPolicy=e.referrerpolicy),"use-credentials"===e.crossorigin?r.credentials="include":"anonymous"===e.crossorigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();const c={};c.render=function(o,n){const s=e("router-view");return r(),t(s)};const a={},d=function(e,r){return r&&0!==r.length?Promise.all(r.map((e=>{if((e=`/cApi/${e}`)in a)return;a[e]=!0;const r=e.endsWith(".css"),t=r?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${e}"]${t}`))return;const o=document.createElement("link");return o.rel=r?"stylesheet":"modulepreload",r||(o.as="script",o.crossOrigin=""),o.href=e,document.head.appendChild(o),r?new Promise(((e,r)=>{o.addEventListener("load",e),o.addEventListener("error",r)})):void 0}))).then((()=>e())):e()},l=[{path:"/",redirect:"/login"},{path:"/login",name:"Login",component:()=>d((()=>import("./Login.ab12ce3d.js")),["assets/Login.ab12ce3d.js","assets/Login.008b81ee.css","assets/vendor.a8167e4a.js"])},{path:"/dashboard",name:"Dashboard",component:()=>d((()=>import("./Dashboard.cd76d04c.js")),[]),meta:{requiresAuth:!0}}],u=o({history:n("/cApi/"),routes:l});s(c).use(i).use(u).mount("#app");
|
||||
4
src/main/resources/static/assets/index.bc67c2d1.js
Normal file
4
src/main/resources/static/assets/index.bc67c2d1.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import{_ as B,a as E,r as d,o as p,c as r,g as e,e as t,h as a,E as A,i as s,w as F,f as m,P as c,Q as C}from"./index.04365c99.js";const _={class:"empty-page-container"},D={class:"empty-content"},v={class:"empty-icon"},y={class:"empty-actions"},f=E({__name:"index",setup(x){const n=d("\u7A7A\u767D\u6A21\u677F\u9875\u9762"),l=()=>{console.log(`[${n.value}] \u89E6\u53D1\u300C\u521B\u5EFA\u5185\u5BB9\u300D\u64CD\u4F5C`),alert("\u53EF\u5728\u6B64\u5904\u5B9E\u73B0\u300C\u521B\u5EFA\u5185\u5BB9\u300D\u7684\u4E1A\u52A1\u903B\u8F91")},i=()=>{console.log(`[${n.value}] \u67E5\u770B\u4F7F\u7528\u6307\u5357`),alert(`\u4F7F\u7528\u6307\u5357\uFF1A
|
||||
1. \u6B64\u9875\u9762\u5DF2\u9002\u914D\u7236\u5BB9\u5668\u81EA\u9002\u5E94\u5C3A\u5BF8
|
||||
2. \u53EF\u5728 empty-content \u5185\u6DFB\u52A0\u4E1A\u52A1\u7EC4\u4EF6
|
||||
3. \u6837\u5F0F\u53EF\u53C2\u8003\u539F\u9879\u76EE\u89C4\u8303\u8C03\u6574`)};return(g,u)=>{const o=m("a-button");return p(),r("div",_,[e("div",D,[e("div",v,[t(a(A))]),u[2]||(u[2]=e("h2",{class:"empty-title"},"\u8FD9\u662F\u4E00\u4E2A\u7A7A\u767D\u9875\u9762",-1)),u[3]||(u[3]=e("p",{class:"empty-desc"},[s(" \u4F60\u53EF\u4EE5\u5728\u8FD9\u91CC\u6269\u5C55\u529F\u80FD\uFF0C\u6BD4\u5982\u6DFB\u52A0\u6570\u636E\u5C55\u793A\u3001\u8868\u5355\u63D0\u4EA4\u3001\u56FE\u8868\u5206\u6790\u7B49\u5185\u5BB9"),e("br"),s(" \u9875\u9762\u5DF2\u9002\u914D\u7236\u5BB9\u5668\u5C3A\u5BF8\uFF0C\u652F\u6301\u9AD8\u5EA6/\u5BBD\u5EA6\u81EA\u9002\u5E94 ")],-1)),e("div",y,[t(o,{type:"primary",onClick:l},{default:F(()=>[t(a(c),{class:"mr-2"}),u[0]||(u[0]=s(" \u521B\u5EFA\u5185\u5BB9 ",-1))]),_:1}),t(o,{onClick:i,style:{"margin-left":"12px"}},{default:F(()=>[t(a(C),{class:"mr-2"}),u[1]||(u[1]=s(" \u67E5\u770B\u4F7F\u7528\u6307\u5357 ",-1))]),_:1})]),u[4]||(u[4]=e("div",{class:"empty-meta"},[e("span",{class:"meta-item"},"\u9875\u9762\u8DEF\u5F84\uFF1A@/views/EmptyTemplatePage.vue"),e("span",{class:"meta-item"},"\u9002\u914D\u573A\u666F\uFF1A\u5217\u8868\u9875\u3001\u8BE6\u60C5\u9875\u3001\u8868\u5355\u9875\u7B49\u521D\u59CB\u6A21\u677F")],-1))])])}}});var N=B(f,[["__scopeId","data-v-2de931e9"]]);export{N as default};
|
||||
1
src/main/resources/static/assets/index.fc5e98cf.css
Normal file
1
src/main/resources/static/assets/index.fc5e98cf.css
Normal file
@@ -0,0 +1 @@
|
||||
html,body{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}@-ms-viewport{width:device-width}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[title],abbr[data-original-title]{-webkit-text-decoration:underline dotted;text-decoration:underline;text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=text],input[type=password],input[type=number],textarea{-webkit-appearance:none}ol,ul,dl{margin-top:0;margin-bottom:1em}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}pre,code,kbd,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}a,area,button,[role=button],input:not([type="range"]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;text-align:left;caption-side:bottom}input,button,select,optgroup,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{padding:0;border-style:none}input[type=radio],input[type=checkbox]{box-sizing:border-box;padding:0}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -4,10 +4,9 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/cApi/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
<script type="module" crossorigin src="/cApi/assets/index.8dd9ceba.js"></script>
|
||||
<link rel="modulepreload" href="/cApi/assets/vendor.a8167e4a.js">
|
||||
<link rel="stylesheet" href="/cApi/assets/index.6a28b391.css">
|
||||
<title>cApi系统管理</title>
|
||||
<script type="module" crossorigin src="/cApi/assets/index.04365c99.js"></script>
|
||||
<link rel="stylesheet" href="/cApi/assets/index.fc5e98cf.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
BIN
src/main/resources/static/my.png
Normal file
BIN
src/main/resources/static/my.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Reference in New Issue
Block a user