前端服务共享

This commit is contained in:
2025-12-11 14:21:36 +08:00
parent fa3dbe0496
commit 5ee9770747
46 changed files with 3732 additions and 1782 deletions

View File

@@ -0,0 +1,27 @@
<template>
<router-view />
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
onMounted(() => {
console.log('Platform App Mounted')
})
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#app {
width: 100%;
height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>

View File

@@ -0,0 +1,254 @@
# Platform 应用配置说明
## AES 加密配置
### 密钥配置
**配置文件**`src/config/index.ts`
```typescript
export const AES_SECRET_KEY = '1234567890qwer'
```
**注意事项**
1. ✅ 密钥已配置为 `1234567890qwer`,与后端保持一致
2. ⚠️ 该密钥与后端 `application.yml` 中的 `security.aes.secret-key` 必须相同
3. 🔒 生产环境应从环境变量或配置中心获取,不要硬编码
### 对应后端配置
**Gateway** (`gateway/src/main/resources/application.yml`):
```yaml
security:
aes:
secret-key: 1234567890qwer
```
**Auth Service** (`auth/src/main/resources/application.yml`):
```yaml
security:
aes:
secret-key: 1234567890qwer
```
## 使用示例
### 1. 登录时加密密码
```typescript
import { authAPI } from '@shared/api/auth'
import { getAesInstance } from '@shared/utils'
async function handleLogin(username: string, password: string) {
try {
// 1. 获取 AES 加密实例
const aes = getAesInstance()
// 2. 加密密码
const encryptedPassword = await aes.encryptPassword(password)
// 3. 发送登录请求
const response = await authAPI.login({
username,
password: encryptedPassword, // 使用加密后的密码
loginType: 'password'
})
if (response.data.success) {
console.log('登录成功')
// 保存 token 等操作
}
} catch (error) {
console.error('登录失败:', error)
}
}
```
### 2. 注册时加密手机号和密码
```typescript
import { authAPI } from '@shared/api/auth'
import { getAesInstance } from '@shared/utils'
async function handleRegister(phone: string, password: string, smsCode: string, sessionId: string) {
try {
const aes = getAesInstance()
// 加密敏感信息
const encryptedPhone = await aes.encryptPhone(phone)
const encryptedPassword = await aes.encryptPassword(password)
// 发送注册请求
const response = await authAPI.register({
registerType: 'phone',
phone: encryptedPhone,
password: encryptedPassword,
confirmPassword: encryptedPassword,
smsCode,
smsSessionId: sessionId
})
if (response.data.success) {
console.log('注册成功')
}
} catch (error) {
console.error('注册失败:', error)
}
}
```
### 3. 数据脱敏显示
```typescript
import { AesUtils } from '@shared/utils'
// 显示脱敏手机号
const phone = '13812345678'
const maskedPhone = AesUtils.maskPhone(phone)
console.log(maskedPhone) // 输出138****5678
// 显示脱敏身份证号
const idCard = '110101199001011234'
const maskedIdCard = AesUtils.maskIdCard(idCard)
console.log(maskedIdCard) // 输出110101********1234
```
## 初始化流程
### 应用启动时自动初始化
**文件**`src/main.ts`
```typescript
import { AES_SECRET_KEY } from './config'
import { initAesEncrypt } from '@shared/utils'
async function initApp() {
// 初始化 AES 加密工具
await initAesEncrypt(AES_SECRET_KEY)
// ... 其他初始化操作
}
initApp()
```
### 初始化状态检查
```typescript
import { getAesInstance } from '@shared/utils'
try {
const aes = getAesInstance()
console.log('✅ AES 加密工具已初始化')
} catch (error) {
console.error('❌ AES 加密工具未初始化:', error)
}
```
## 加密流程图
```
用户输入密码
前端 AES 加密 (1234567890qwer)
发送加密后的密码
Gateway (不解密,直接转发)
Auth Service 接收
AES 解密 (1234567890qwer)
BCrypt 再次加密
存入数据库
```
## 安全建议
### 开发环境
- ✅ 使用固定密钥 `1234567890qwer`
- ✅ 密钥在代码中配置
### 生产环境
- 🔒 从环境变量获取密钥
- 🔒 使用配置中心Nacos
- 🔒 定期轮换密钥
- 🔒 密钥长度至少 32 字符
### 示例:从环境变量获取
```typescript
// 生产环境配置
export const AES_SECRET_KEY = process.env.VUE_APP_AES_SECRET_KEY || '1234567890qwer'
```
## 故障排查
### 问题:登录时提示"密码错误"
**可能原因**:前后端密钥不一致
**排查步骤**
1. 检查前端配置:`src/config/index.ts` 中的 `AES_SECRET_KEY`
2. 检查后端配置:`application.yml` 中的 `security.aes.secret-key`
3. 确保两者完全一致
**解决方案**
```bash
# 前端
export const AES_SECRET_KEY = '1234567890qwer'
# 后端
security:
aes:
secret-key: 1234567890qwer
```
### 问题:"AES 加密工具未初始化"
**原因**`initAesEncrypt()` 未被调用
**解决**:检查 `main.ts` 中是否正确调用初始化函数
### 问题:加密后的数据无法解密
**可能原因**
1. 密钥不正确
2. 数据被篡改
3. Base64 编码问题
**调试方法**
```typescript
const aes = getAesInstance()
const original = 'test123'
const encrypted = await aes.encrypt(original)
const decrypted = await aes.decrypt(encrypted)
console.log(original === decrypted) // 应该输出 true
```
## API 参考
### 配置项
| 配置项 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `AES_SECRET_KEY` | `string` | `'1234567890qwer'` | AES 加密密钥 |
| `API_BASE_URL` | `string` | `'http://localhost:8180'` | API 基础地址 |
| `APP_CONFIG.name` | `string` | `'泰豪电源 AI 数智化平台'` | 应用名称 |
| `APP_CONFIG.version` | `string` | `'1.0.0'` | 应用版本 |
### 环境变量
| 变量名 | 说明 | 示例 |
|--------|------|------|
| `VITE_API_BASE_URL` | API 基础地址 | `https://api.example.com` |
| `VUE_APP_AES_SECRET_KEY` | AES 密钥(生产) | `your-secret-key-32-chars-long` |
## 更多信息
- AES 加密工具详细文档:`@shared/utils/crypto/README.md`
- Auth API 文档:`@shared/api/auth/auth.ts`
- 后端 AES 实现:`urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/crypto/AesEncryptUtil.java`

View File

@@ -0,0 +1,24 @@
/**
* Platform 应用配置
*/
/**
* AES 加密密钥(与后端保持一致)
* 对应后端配置security.aes.secret-key
* Base64 编码的 32 字节密钥256 位)
*/
export const AES_SECRET_KEY = 'MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=' // Base64 编码,解码后是 "12345678901234567890123456789012" (32字节)
/**
* API 基础地址
*/
export const API_BASE_URL = (import.meta as any).env?.VITE_API_BASE_URL || 'http://localhost:8180'
/**
* 应用配置
*/
export const APP_CONFIG = {
name: '泰豪电源 AI 数智化平台',
version: '1.0.0',
copyright: '泰豪电源'
}

View File

@@ -0,0 +1,214 @@
# SidebarLayout 侧边栏布局组件
## 功能特性
### 🎯 核心功能
- **侧边栏菜单导航**:左侧固定侧边栏,支持折叠/展开
- **双模式内容区**
- **路由模式**:普通路由页面渲染
- **iframe 模式**:嵌入外部应用(招标助手、泰豪小电、智能体编排)
- **用户信息展示**:底部用户头像和下拉菜单
- **响应式设计**:支持移动端适配
### 📱 菜单配置
```typescript
const menuItems: MenuItem[] = [
{
key: 'home',
label: '工作台',
icon: 'Grid',
path: '/home',
type: 'route'
},
{
key: 'bidding',
label: '招标助手',
icon: 'Document',
iframeUrl: 'http://localhost:5002',
type: 'iframe'
},
{
key: 'service',
label: '泰豪小电',
icon: 'Service',
iframeUrl: 'http://localhost:5003',
type: 'iframe'
},
{
key: 'workflow',
label: '智能体编排',
icon: 'Connection',
iframeUrl: 'http://localhost:3000', // Dify 地址
type: 'iframe'
}
]
```
## 使用方式
### 在路由中使用
```typescript
// router/index.ts
import { SidebarLayout } from '@/layouts'
const routes = [
{
path: '/',
component: SidebarLayout,
children: [
{
path: '/home',
component: () => import('@/views/Home.vue')
},
{
path: '/chat',
component: () => import('@/views/Chat.vue')
}
]
}
]
```
### 在 App.vue 中使用
```vue
<template>
<SidebarLayout />
</template>
<script setup lang="ts">
import { SidebarLayout } from '@/layouts'
</script>
```
## 菜单项类型
```typescript
interface MenuItem {
key: string // 唯一标识
label: string // 显示名称
icon: string // Element Plus 图标组件名
path?: string // 路由路径route 类型必需)
iframeUrl?: string // iframe URLiframe 类型必需)
type: 'route' | 'iframe' // 菜单类型
}
```
## iframe 应用说明
### 1. 招标助手 (Bidding)
- **端口**5002
- **URL**http://localhost:5002
- **说明**:招投标业务管理系统
### 2. 泰豪小电 (Service)
- **端口**5003
- **URL**http://localhost:5003
- **说明**:智能客服工单管理系统
### 3. 智能体编排 (Workflow)
- **端口**3000
- **URL**http://localhost:3000
- **说明**Dify 智能体编排界面
## 样式自定义
### 主题色调整
```scss
// 修改侧边栏背景色
.sidebar {
background: #F0EAF4; // 当前淡紫色背景
}
// 修改激活项颜色
.nav-item.active {
background: rgba(124, 58, 237, 0.15);
color: #7c3aed;
}
```
### 侧边栏宽度
```scss
.sidebar {
width: 220px; // 展开宽度
&.collapsed {
width: 64px; // 折叠宽度
}
}
```
## 功能说明
### 侧边栏折叠
- 点击头部箭头图标可切换折叠/展开状态
- 折叠后只显示图标,展开后显示图标+文字
### iframe 加载
- 自动显示加载中状态
- 支持刷新按钮重新加载
- 显示当前应用标题
### 用户操作
- **个人中心**:跳转到 /profile
- **系统设置**:跳转到 /settings
- **退出登录**:跳转到 /login
## 注意事项
1. **跨域问题**:确保 iframe 应用允许被嵌入
```nginx
# nginx 配置
add_header X-Frame-Options "SAMEORIGIN";
# 或者
add_header Content-Security-Policy "frame-ancestors 'self' http://localhost:5001";
```
2. **端口配置**:确保对应服务已启动
- platform: 5001
- bidding: 5002
- workcase: 5003
- dify: 3000
3. **路由同步**iframe 模式不会改变浏览器 URL
4. **通信机制**:如需与 iframe 通信,使用 postMessage API
## 扩展建议
### 添加新菜单项
```typescript
// 在 menuItems 数组中添加
{
key: 'new-app',
label: '新应用',
icon: 'Plus',
iframeUrl: 'http://localhost:5004',
type: 'iframe'
}
```
### 动态菜单加载
```typescript
// 从 API 获取菜单配置
const loadMenus = async () => {
const response = await fetch('/api/menus')
menuItems.value = await response.json()
}
```
### 权限控制
```typescript
const menuItems = computed(() => {
return allMenuItems.filter(item =>
hasPermission(item.key)
)
})
```

View File

@@ -0,0 +1,264 @@
.sidebar-layout {
display: flex;
width: 100%;
height: 100vh;
overflow: hidden;
}
// ==================== 侧边栏 ====================
.sidebar {
width: 220px;
height: 100%;
background: #F0EAF4;
display: flex;
flex-direction: column;
color: #333;
flex-shrink: 0;
transition: width 0.3s ease;
border-right: 1px solid rgba(0, 0, 0, 0.08);
&.collapsed {
width: 64px;
.sidebar-header {
padding: 16px 12px;
justify-content: center;
.logo {
justify-content: center;
}
.collapse-btn {
position: static;
margin-left: 0;
}
}
.nav-item {
justify-content: center;
padding: 12px;
}
.user-section {
justify-content: center;
padding: 16px 12px;
}
}
}
// 侧边栏头部
.sidebar-header {
padding: 16px 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: space-between;
}
.collapse-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
color: #888;
transition: all 0.2s;
&:hover {
background: rgba(124, 58, 237, 0.1);
color: #7c3aed;
}
}
.logo {
display: flex;
align-items: center;
gap: 10px;
.logo-img {
width: 40px;
height: 40px;
border-radius: 6px;
object-fit: contain;
background: #fff;
padding: 2px;
}
.logo-text {
font-size: 16px;
font-weight: 600;
color: #333;
}
}
// 导航菜单
.nav-menu {
flex: 1;
overflow-y: auto;
padding: 12px 0;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
}
.nav-section {
padding: 8px 0;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 20px;
margin-bottom: 4px;
cursor: pointer;
transition: all 0.2s ease;
color: #555;
font-size: 14px;
&:hover {
background: rgba(124, 58, 237, 0.1);
color: #7c3aed;
}
&.active {
background: rgba(124, 58, 237, 0.15);
color: #7c3aed;
font-weight: 500;
}
.el-icon {
font-size: 18px;
flex-shrink: 0;
}
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
// 用户信息
.user-section {
padding: 16px 20px;
border-top: 1px solid rgba(0, 0, 0, 0.08);
cursor: pointer;
transition: background 0.2s;
&:hover {
background: rgba(124, 58, 237, 0.05);
}
.user-info-wrapper {
display: flex;
align-items: center;
gap: 12px;
}
.user-avatar {
flex-shrink: 0;
}
.user-name {
font-size: 14px;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
// ==================== 主内容区 ====================
.main-content {
flex: 1;
height: 100%;
overflow: hidden;
background: #fff;
position: relative;
}
// iframe 容器
.iframe-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
position: relative;
}
.iframe-header {
height: 56px;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e5e7eb;
background: #fafafa;
flex-shrink: 0;
}
.iframe-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.content-iframe {
flex: 1;
width: 100%;
height: 100%;
border: none;
background: #fff;
}
.iframe-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #7c3aed;
font-size: 14px;
z-index: 10;
.el-icon {
font-size: 32px;
}
}
// ==================== 响应式 ====================
@media (max-width: 768px) {
.sidebar {
width: 64px;
&:not(.collapsed) {
width: 220px;
position: fixed;
left: 0;
top: 0;
z-index: 1000;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
}
}
.iframe-header {
padding: 0 16px;
.iframe-title {
font-size: 14px;
}
}
}

View File

@@ -0,0 +1,248 @@
<template>
<div class="sidebar-layout">
<!-- 侧边栏 -->
<aside class="sidebar" :class="{ collapsed: collapsed }">
<div class="sidebar-header">
<div class="logo">
<img src="/logo.jpg" alt="Logo" class="logo-img" />
<span v-if="!collapsed" class="logo-text">城市生命线</span>
</div>
<div class="collapse-btn" @click="toggleSidebar">
<el-icon>
<DArrowLeft v-if="!collapsed" />
<DArrowRight v-else />
</el-icon>
</div>
</div>
<nav class="nav-menu">
<div class="nav-section">
<div
v-for="item in menuItems"
:key="item.key"
class="nav-item"
:class="{ active: activeMenu === item.key }"
@click="handleMenuClick(item)"
>
<el-icon><component :is="item.icon" /></el-icon>
<span v-if="!collapsed">{{ item.label }}</span>
</div>
</div>
</nav>
<!-- 用户信息 -->
<el-dropdown class="user-section" trigger="click" @command="handleUserCommand">
<div class="user-info-wrapper">
<div class="user-avatar">
<el-avatar :size="36" src="/avatar.svg" @error="handleAvatarError" />
</div>
<span v-if="!collapsed" class="user-name">{{ userName }}</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
<el-icon><User /></el-icon>
个人中心
</el-dropdown-item>
<el-dropdown-item command="settings" divided>
<el-icon><Setting /></el-icon>
系统设置
</el-dropdown-item>
<el-dropdown-item command="logout" divided>
<el-icon><SwitchButton /></el-icon>
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</aside>
<!-- 主内容区 -->
<main class="main-content">
<!-- iframe 模式 -->
<div v-if="currentIframeUrl" class="iframe-container">
<div class="iframe-header">
<span class="iframe-title">{{ currentMenuItem?.label }}</span>
<el-button
text
@click="handleRefreshIframe"
:icon="Refresh"
>
刷新
</el-button>
</div>
<iframe
ref="iframeRef"
:src="currentIframeUrl"
class="content-iframe"
frameborder="0"
@load="handleIframeLoad"
/>
<div v-if="iframeLoading" class="iframe-loading">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
</div>
<!-- 路由模式 -->
<router-view v-else />
</main>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import {
ChatDotRound,
Grid,
Connection,
Document,
Service,
DArrowLeft,
DArrowRight,
User,
Setting,
SwitchButton,
Refresh,
Loading
} from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
interface MenuItem {
key: string
label: string
icon: string
path?: string
iframeUrl?: string
type: 'route' | 'iframe'
}
const router = useRouter()
const route = useRoute()
// 状态管理
const collapsed = ref(false)
const activeMenu = ref('home')
const iframeLoading = ref(false)
const iframeRef = ref<HTMLIFrameElement>()
const userName = ref('管理员')
// 菜单配置
const menuItems: MenuItem[] = [
{
key: 'home',
label: '工作台',
icon: 'Grid',
path: '/home',
type: 'route'
},
{
key: 'bidding',
label: '招标助手',
icon: 'Document',
iframeUrl: 'http://localhost:5002',
type: 'iframe'
},
{
key: 'service',
label: '泰豪小电',
icon: 'Service',
iframeUrl: 'http://localhost:5003',
type: 'iframe'
},
{
key: 'workflow',
label: '智能体编排',
icon: 'Connection',
iframeUrl: 'http://localhost:3000',
type: 'iframe'
},
{
key: 'chat',
label: 'AI助手',
icon: 'ChatDotRound',
path: '/chat',
type: 'route'
}
]
// 当前菜单项
const currentMenuItem = computed(() => {
return menuItems.find(item => item.key === activeMenu.value)
})
// 当前 iframe URL
const currentIframeUrl = computed(() => {
return currentMenuItem.value?.type === 'iframe'
? currentMenuItem.value.iframeUrl
: null
})
// 切换侧边栏
const toggleSidebar = () => {
collapsed.value = !collapsed.value
}
// 处理菜单点击
const handleMenuClick = (item: MenuItem) => {
activeMenu.value = item.key
if (item.type === 'route' && item.path) {
router.push(item.path)
} else if (item.type === 'iframe') {
iframeLoading.value = true
// iframe 模式不需要路由跳转
}
}
// iframe 加载完成
const handleIframeLoad = () => {
iframeLoading.value = false
}
// 刷新 iframe
const handleRefreshIframe = () => {
if (iframeRef.value) {
iframeLoading.value = true
iframeRef.value.src = iframeRef.value.src
}
}
// 用户头像加载错误
const handleAvatarError = () => {
return true
}
// 用户操作
const handleUserCommand = (command: string) => {
switch (command) {
case 'profile':
router.push('/profile')
break
case 'settings':
router.push('/settings')
break
case 'logout':
ElMessage.success('退出成功')
router.push('/login')
break
}
}
// 监听路由变化,同步激活菜单
watch(
() => route.path,
(newPath) => {
const menuItem = menuItems.find(item => item.path === newPath)
if (menuItem) {
activeMenu.value = menuItem.key
}
},
{ immediate: true }
)
</script>
<style lang="scss" scoped>
@import url("./SidebarLayout.scss");
</style>

View File

@@ -0,0 +1 @@
export { default as SidebarLayout } from "./SidebarLayout/SidebarLayout.vue";

View File

@@ -0,0 +1,47 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router/'
import { AES_SECRET_KEY } from './config'
import { initAesEncrypt } from 'shared/utils'
// 异步初始化应用
async function initApp() {
// 1. 初始化 AES 加密工具
try {
await initAesEncrypt(AES_SECRET_KEY)
console.log('✅ AES 加密工具初始化成功')
} catch (error) {
console.error('❌ AES 加密工具初始化失败:', error)
}
// 2. 创建 Vue 应用
const app = createApp(App)
// 3. 注册 Pinia
const pinia = createPinia()
app.use(pinia)
// 4. 注册 Element Plus
app.use(ElementPlus)
// 5. 注册所有 Element Plus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// 6. 注册路由
app.use(router)
// 7. 挂载应用
app.mount('#app')
console.log('✅ Platform 应用启动成功')
}
// 启动应用
initApp()

View File

@@ -0,0 +1,60 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { SidebarLayout } from '../layouts'
import { TokenManager } from 'shared/api'
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/home'
},
{
path: '/login',
name: 'Login',
component: () => import('../views/public/Login.vue'),
meta: {
title: '登录',
requiresAuth: false // 不需要登录
}
},
{
path: '/home',
name: 'Home',
component: SidebarLayout,
meta: {
title: '首页',
requiresAuth: true // 需要登录
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
// 设置页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - 泰豪电源 AI 数智化平台`
}
// 检查是否需要登录
const requiresAuth = to.meta.requiresAuth !== false // 默认需要登录
const hasToken = TokenManager.hasToken()
if (requiresAuth && !hasToken) {
// 需要登录但未登录,跳转到登录页
next({
path: '/login',
query: { redirect: to.fullPath } // 保存原始路径
})
} else if (to.path === '/login' && hasToken) {
// 已登录但访问登录页,跳转到首页
next('/home')
} else {
next()
}
})
export default router

View File

@@ -0,0 +1,54 @@
/**
* Shared Module Federation 类型声明
* 用于 TypeScript 识别远程模块
*/
declare module 'shared/FileUpload' {
import { DefineComponent } from 'vue'
const FileUpload: DefineComponent<{}, {}, any>
export default FileUpload
}
declare module 'shared/DynamicFormItem' {
import { DefineComponent } from 'vue'
const DynamicFormItem: DefineComponent<{}, {}, any>
export default DynamicFormItem
}
declare module 'shared/api' {
export const api: any
export const TokenManager: any
}
declare module 'shared/authAPI' {
export const authAPI: any
}
declare module 'shared/fileAPI' {
export const fileAPI: any
}
declare module 'shared/utils' {
export const initAesEncrypt: any
export const getAesInstance: any
export const formatFileSize: any
export const isImageFile: any
export const getFileTypeIcon: any
export const isValidFileType: any
export const getFilePreviewUrl: any
}
declare module 'shared/types' {
export type LoginParam = any
export type LoginDomain = any
export type SysUserVO = any
export type TbSysFileDTO = any
export type SysConfigVO = any
export type ResultDomain<T = any> = any
}
declare module 'shared/components' {
export const FileUpload: any
export const DynamicFormItem: any
}

View File

@@ -1,156 +0,0 @@
<script setup lang="ts">
/**
* Import Maps 使用示例
*
* 通过 HTML 中的 <script type="importmap"> 配置,
* 可以直接从 HTTP URL 导入共享组件
*/
// ✅ 直接导入!浏览器会自动从 http://localhost/shared/components.js 加载
import { UlTable } from '@shared/components'
import { http } from '@shared/utils'
import { authApi } from '@shared/api'
import { useTable } from '@shared/composables'
// 类型定义
interface User {
id: string
username: string
email: string
createTime: string
}
// 使用共享的 useTable 组合式函数
const {
loading,
tableData,
pagination,
handlePageChange,
handleSizeChange,
refresh
} = useTable<User>({
fetchData: async (params) => {
// 使用共享的 API 函数
return await authApi.getUserList(params)
}
})
// 表格列配置
const columns = [
{ prop: 'username', label: '用户名', minWidth: 150 },
{ prop: 'email', label: '邮箱', minWidth: 200 },
{ prop: 'createTime', label: '创建时间', width: 180 }
]
// 测试 HTTP 请求
const testRequest = async () => {
try {
// 使用共享的 http 工具
const result = await http.get('/api/test')
console.log('请求结果:', result)
} catch (error) {
console.error('请求失败:', error)
}
}
</script>
<template>
<div class="import-maps-example">
<div class="header">
<h1>Import Maps 示例</h1>
<p class="description">
共享组件从 <code>http://localhost/shared/</code> 动态加载<br>
无需打包浏览器原生 ES Module 支持<br>
真正的运行时共享所有应用使用同一份代码
</p>
<el-button type="primary" @click="testRequest">
测试 HTTP 请求
</el-button>
</div>
<!-- 使用从 HTTP 加载的共享组件 -->
<UlTable
:data="tableData"
:columns="columns"
:loading="loading"
:pagination="pagination"
@page-change="handlePageChange"
@size-change="handleSizeChange"
/>
<div class="info">
<h3>📦 当前加载的模块</h3>
<ul>
<li><code>@shared/components</code> http://localhost/shared/components.js</li>
<li><code>@shared/utils</code> http://localhost/shared/utils.js</li>
<li><code>@shared/api</code> http://localhost/shared/api.js</li>
<li><code>@shared/composables</code> http://localhost/shared/composables.js</li>
</ul>
<h3>🔍 如何查看</h3>
<ol>
<li>打开浏览器开发者工具 (F12)</li>
<li>切换到 Network 标签页</li>
<li>筛选 JS 类型</li>
<li>刷新页面可以看到从 /shared/ 加载的模块</li>
</ol>
<h3> 优势</h3>
<ul>
<li> 真正的代码共享所有应用共用一份</li>
<li> 支持热更新修改共享组件所有应用自动更新</li>
<li> 减小构建体积共享代码不打包到业务应用</li>
<li> 浏览器缓存共享模块只下载一次</li>
</ul>
</div>
</div>
</template>
<style scoped>
.import-maps-example {
padding: 24px;
}
.header {
margin-bottom: 24px;
}
.description {
color: #666;
line-height: 1.8;
margin: 16px 0;
}
.description code {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
.info {
margin-top: 32px;
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.info h3 {
margin-top: 20px;
margin-bottom: 12px;
color: #409eff;
}
.info ul, .info ol {
line-height: 2;
color: #606266;
}
.info code {
background: #fff;
padding: 2px 8px;
border-radius: 3px;
font-family: 'Courier New', monospace;
color: #e6a23c;
}
</style>

View File

@@ -1,24 +1,256 @@
<template>
<div>
<div class="login-container">
<div class="login-card">
<div class="login-header">
<h1 class="login-title">泰豪电源 AI 数智化平台</h1>
<p class="login-subtitle">Urban Lifeline Platform</p>
</div>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="login-form"
@submit.prevent="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
size="large"
prefix-icon="User"
clearable
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
size="large"
prefix-icon="Lock"
show-password
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item>
<el-checkbox v-model="loginForm.rememberMe">
记住我
</el-checkbox>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
class="login-button"
@click="handleLogin"
>
{{ loading ? '登录中...' : '登录' }}
</el-button>
</el-form-item>
</el-form>
<div class="login-footer">
<span class="footer-link">忘记密码</span>
<span class="footer-divider">|</span>
<span class="footer-link">注册账号</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from "vue"
import type { LoginParam} from "@shared/types/auth"
import { authAPI } from "@shared/api/auth/auth"
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import type { LoginParam } from 'shared/types'
import { authAPI } from 'shared/authAPI'
import { getAesInstance } from 'shared/utils'
import { TokenManager } from 'shared/api'
// 路由
const router = useRouter()
const loginParam = reactive<LoginParam>({
loginType: "password"
// 表单引用
const loginFormRef = ref<FormInstance>()
// 加载状态
const loading = ref(false)
// 登录表单
const loginForm = reactive({
username: '',
password: '',
rememberMe: false
})
async function login(){
// const result = await authAPI.login(loginParam)
// 表单验证规则
const loginRules: FormRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度为3-20个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码至少6个字符', trigger: 'blur' }
]
}
/**
* 处理登录
*/
async function handleLogin() {
if (!loginFormRef.value) return
try {
// 1. 表单验证
await loginFormRef.value.validate()
// 2. 显示加载状态
loading.value = true
// 3. 获取 AES 加密实例
const aes = getAesInstance()
// 4. 加密密码
const encryptedPassword = await aes.encryptPassword(loginForm.password)
// 5. 构建登录参数
const loginParam: LoginParam = {
username: loginForm.username,
password: encryptedPassword, // 使用加密后的密码
loginType: 'password'
}
// 6. 调用登录接口
const response = await authAPI.login(loginParam)
// 7. 检查登录结果
if (response.data.success && response.data.data) {
const loginData = response.data.data
// 8. 保存 Token
if (loginData.token) {
TokenManager.setToken(loginData.token, loginForm.rememberMe)
}
// 9. 显示成功消息
ElMessage.success('登录成功!')
// 10. 跳转到首页
router.push('/home')
} else {
// 登录失败
ElMessage.error(response.data.message || '登录失败,请检查用户名和密码')
}
} catch (error: any) {
console.error('登录失败:', error)
// 显示错误消息
if (error.response) {
// HTTP 错误
ElMessage.error(error.response.data?.message || '登录失败,请稍后重试')
} else if (error.message) {
// 其他错误
ElMessage.error(error.message)
} else {
ElMessage.error('登录失败,请检查网络连接')
}
} finally {
// 隐藏加载状态
loading.value = false
}
}
</script>
<style lang="scss" scoped>
.login-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.login-card {
width: 100%;
max-width: 420px;
padding: 40px;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.login-header {
text-align: center;
margin-bottom: 40px;
}
.login-title {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.login-subtitle {
font-size: 14px;
color: #999;
}
.login-form {
:deep(.el-form-item) {
margin-bottom: 24px;
}
:deep(.el-input__wrapper) {
padding: 12px 16px;
}
}
.login-button {
width: 100%;
height: 44px;
font-size: 16px;
font-weight: 500;
border-radius: 8px;
}
.login-footer {
display: flex;
align-items: center;
justify-content: center;
margin-top: 24px;
font-size: 14px;
color: #999;
}
.footer-link {
cursor: pointer;
transition: color 0.3s;
&:hover {
color: var(--el-color-primary);
}
}
.footer-divider {
margin: 0 12px;
}
// 响应式设计
@media (max-width: 480px) {
.login-card {
padding: 30px 20px;
}
.login-title {
font-size: 20px;
}
}
</style>