初始化前端目录

This commit is contained in:
2025-09-03 21:42:51 +08:00
parent de83878345
commit 31ebed0b5e
68 changed files with 4230 additions and 3461 deletions

3329
capi-ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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
View 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 || []
}

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -1,6 +1,4 @@
<template>
ffffffffffffffffffff
</template>
<script>

View 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;

View File

@@ -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')

View File

@@ -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

View File

@@ -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
View 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 '访客';
}

View 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>

View File

@@ -0,0 +1,9 @@
<template>
app
</template>
<script>
</script>
<style>
</style>

View 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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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

View File

@@ -1,4 +1,5 @@
<template>
顶顶顶顶顶顶顶顶顶顶顶顶顶顶
</template>
<script>

View File

@@ -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;
}
}

View File

@@ -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/**"
);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
/* ---------------- 静态工厂方法 ---------------- */
/**

View 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);
}
}

View File

@@ -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);
}

View 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 {
}

View File

@@ -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() {

View File

@@ -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;
}
}
}

View File

@@ -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";
}
}

View File

@@ -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";
// }
}

View File

@@ -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";
}
}

View 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;
}
}

View File

@@ -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")
);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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;
}
/**
* AES256解密
*
* @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;
}
}

View 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();
}
}
}

View File

@@ -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

View File

@@ -1 +0,0 @@
const n={};n.render=function(n,e){return null};export{n as default};

View File

@@ -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}

View 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

View 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};

View 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};

View 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};

File diff suppressed because one or more lines are too long

View 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}

View 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};

View 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};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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}

View 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}}

View File

@@ -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");

View 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};

View 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

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB