chore: initial baseline with P0-safety .gitignore

This commit is contained in:
Simon
2026-06-14 16:49:18 +08:00
commit 63262292d7
510 changed files with 146008 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IT智能服务台 - 选择工作台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+1980
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "wecom-it-desk-portal",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.3.0",
"pinia": "^2.1.0",
"axios": "^1.7.0",
"element-plus": "^2.7.0",
"@element-plus/icons-vue": "^2.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"typescript": "^5.5.0",
"vite": "^5.3.0",
"vue-tsc": "^2.0.0"
}
}
+49
View File
@@ -0,0 +1,49 @@
<template>
<!-- 路由出口 -->
<router-view />
</template>
<script setup lang="ts">
// App 根组件,仅作为路由出口
</script>
<style>
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 深色科技风背景 */
body {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
min-height: 100vh;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #1e293b;
}
::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #64748b;
}
</style>
+51
View File
@@ -0,0 +1,51 @@
// =============================================================================
// IT智能服务台 — Portal API 层
// =============================================================================
// 说明:封装所有与后端 API 的交互
// =============================================================================
import axios from 'axios'
// 创建 axios 实例
const apiClient = axios.create({
baseURL: '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
// 请求拦截器:添加 Token
apiClient.interceptors.request.use(
(config) => {
// 从 localStorage 获取 Token
const token = localStorage.getItem('portal_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器:处理错误
apiClient.interceptors.response.use(
(response) => {
return response
},
(error) => {
// 401 错误:Token 过期或无效
if (error.response?.status === 401) {
// 清除本地存储的 Token
localStorage.removeItem('portal_token')
localStorage.removeItem('portal_user')
// 跳转到选择页
window.location.href = '/itportal/select'
}
return Promise.reject(error)
}
)
export default apiClient
+67
View File
@@ -0,0 +1,67 @@
// =============================================================================
// IT智能服务台 — Portal API 接口
// =============================================================================
// 说明:Portal 统一入口相关的 API 接口
// =============================================================================
import apiClient from './index'
// 角色信息接口
export interface Role {
id: string
name: string
display_name: string
description: string | null
permissions: string[]
is_default: boolean
user_count: number | null
created_at: string
updated_at: string
}
// 用户信息接口
export interface UserInfo {
employee_id: string
name: string
department: string | null
avatar: string | null
roles: Role[]
current_role: string
}
// 角色切换响应接口
export interface SwitchRoleResponse {
current_role: string
redirect_url: string
}
/**
* 获取当前用户角色信息
* @returns 用户信息(包含角色列表)
*/
export async function getUserRoles(): Promise<UserInfo> {
const response = await apiClient.get('/portal/roles')
return response.data.data
}
/**
* 切换当前角色
* @param newRole 目标角色标识
* @returns 切换结果(包含重定向URL)
*/
export async function switchRole(newRole: string): Promise<SwitchRoleResponse> {
const response = await apiClient.post('/portal/switch-role', {
new_role: newRole,
})
return response.data.data
}
/**
* 获取角色对应的入口 URL
* @param roleName 角色标识
* @returns 入口 URL 信息
*/
export async function getRoleEntry(roleName: string): Promise<{ role: string; url: string; display_name: string }> {
const response = await apiClient.get(`/portal/entry/${roleName}`)
return response.data.data
}
+39
View File
@@ -0,0 +1,39 @@
// =============================================================================
// IT智能服务台 — Portal 统一入口前端应用
// =============================================================================
// 说明:统一入口前端应用,负责:
// 1. OAuth2 认证后获取用户角色
// 2. 展示角色选择页面(卡片选择)
// 3. 根据用户选择跳转到对应端
// =============================================================================
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
// 导入 Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 创建 Vue 应用实例
const app = createApp(App)
// 注册 Element Plus
app.use(ElementPlus)
// 注册所有 Element Plus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// 注册 Pinia 状态管理
app.use(createPinia())
// 注册路由
app.use(router)
// 挂载应用
app.mount('#app')
+59
View File
@@ -0,0 +1,59 @@
// =============================================================================
// IT智能服务台 — Portal 路由配置
// =============================================================================
// 说明:Portal 统一入口的路由配置
// =============================================================================
import { createRouter, createWebHistory } from 'vue-router'
// 路由配置
const routes = [
{
// 根路径重定向到角色选择页
path: '/',
redirect: '/select',
},
{
// 角色选择页
path: '/select',
name: 'PortalSelect',
component: () => import('@/views/PortalSelect.vue'),
meta: {
title: '选择工作台',
},
},
{
// 加载中页面
path: '/loading',
name: 'PortalLoading',
component: () => import('@/views/PortalLoading.vue'),
meta: {
title: '正在加载...',
},
},
{
// 404 页面
path: '/:pathMatch(.*)*',
name: 'NotFound',
redirect: '/select',
},
]
// 创建路由实例
const router = createRouter({
// 使用 history 模式
history: createWebHistory('/itportal/'),
routes,
})
// 路由守卫:设置页面标题
router.beforeEach((to, _from, next) => {
// 设置页面标题
const title = to.meta.title as string
if (title) {
document.title = `${title} - IT智能服务台`
}
next()
})
export default router
+174
View File
@@ -0,0 +1,174 @@
// =============================================================================
// IT智能服务台 — Portal 状态管理
// =============================================================================
// 说明:Portal 统一入口的状态管理,负责:
// 1. 用户信息和角色管理
// 2. Token 管理
// 3. 角色切换
// =============================================================================
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { getUserRoles, switchRole as apiSwitchRole, type UserInfo, type Role } from '@/api/portal'
/**
* Portal 状态管理 Store
*/
export const usePortalStore = defineStore('portal', () => {
// ==================== 状态 ====================
// 用户信息
const userInfo = ref<UserInfo | null>(null)
// 加载状态
const loading = ref(false)
// 错误信息
const error = ref<string | null>(null)
// ==================== 计算属性 ====================
// Token
const token = computed(() => localStorage.getItem('portal_token'))
// 是否已认证
const isAuthenticated = computed(() => !!token.value)
// 用户角色列表
const roles = computed(() => userInfo.value?.roles || [])
// 当前选择的角色
const currentRole = computed(() => userInfo.value?.current_role || 'user')
// 是否有坐席角色
const hasAgentRole = computed(() => roles.value.some((r: Role) => r.name === 'agent'))
// 是否有管理员角色
const hasAdminRole = computed(() => roles.value.some((r: Role) => r.name === 'admin'))
// 角色数量(用于决定是否显示选择页)
const roleCount = computed(() => roles.value.length)
// ==================== 方法 ====================
/**
* 设置 Token
* @param newToken Token 字符串
*/
function setToken(newToken: string) {
localStorage.setItem('portal_token', newToken)
}
/**
* 清除认证信息
*/
function clearAuth() {
localStorage.removeItem('portal_token')
localStorage.removeItem('portal_user')
userInfo.value = null
}
/**
* 获取用户角色信息
* 从后端 API 获取当前用户的角色列表
*/
async function fetchUserInfo() {
if (!token.value) {
error.value = '未登录'
return
}
loading.value = true
error.value = null
try {
const data = await getUserRoles()
userInfo.value = data
// 缓存到 localStorage
localStorage.setItem('portal_user', JSON.stringify(data))
} catch (err: any) {
console.error('获取用户信息失败:', err)
error.value = err.response?.data?.detail || '获取用户信息失败'
// 如果是 401 错误,清除认证信息
if (err.response?.status === 401) {
clearAuth()
}
} finally {
loading.value = false
}
}
/**
* 切换角色
* @param newRole 目标角色标识
*/
async function switchToRole(newRole: string) {
if (!token.value) {
error.value = '未登录'
return
}
loading.value = true
error.value = null
try {
const result = await apiSwitchRole(newRole)
// 更新本地状态
if (userInfo.value) {
userInfo.value.current_role = result.current_role
}
// 跳转到目标页面
window.location.href = result.redirect_url
} catch (err: any) {
console.error('角色切换失败:', err)
error.value = err.response?.data?.detail || '角色切换失败'
} finally {
loading.value = false
}
}
/**
* 从缓存恢复用户信息
* 用于页面刷新时恢复状态
*/
function restoreFromCache() {
const cachedUser = localStorage.getItem('portal_user')
if (cachedUser) {
try {
userInfo.value = JSON.parse(cachedUser)
} catch (e) {
console.error('解析缓存用户信息失败:', e)
localStorage.removeItem('portal_user')
}
}
}
// ==================== 返回 ====================
return {
// 状态
userInfo,
loading,
error,
// 计算属性
token,
isAuthenticated,
roles,
currentRole,
hasAgentRole,
hasAdminRole,
roleCount,
// 方法
setToken,
clearAuth,
fetchUserInfo,
switchToRole,
restoreFromCache,
}
})
@@ -0,0 +1,96 @@
<template>
<!-- 加载中页面 -->
<div class="portal-loading">
<div class="loading-content">
<!-- 加载动画 -->
<div class="loading-spinner">
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
</div>
<!-- 加载文本 -->
<h2 class="loading-title">正在加载...</h2>
<p class="loading-subtitle">请稍候正在准备您的工作台</p>
</div>
</div>
</template>
<script setup lang="ts">
// 加载中页面,无特殊逻辑
</script>
<style scoped>
/* 页面容器 */
.portal-loading {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
/* 加载内容 */
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
/* 加载动画 */
.loading-spinner {
position: relative;
width: 80px;
height: 80px;
}
.spinner-ring {
position: absolute;
width: 100%;
height: 100%;
border: 3px solid transparent;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 1.5s linear infinite;
}
.spinner-ring:nth-child(2) {
width: 60px;
height: 60px;
top: 10px;
left: 10px;
border-top-color: #f59e0b;
animation-duration: 1.2s;
animation-direction: reverse;
}
.spinner-ring:nth-child(3) {
width: 40px;
height: 40px;
top: 20px;
left: 20px;
border-top-color: #10b981;
animation-duration: 0.9s;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 加载文本 */
.loading-title {
font-size: 24px;
font-weight: 600;
color: #f1f5f9;
}
.loading-subtitle {
font-size: 14px;
color: #94a3b8;
}
</style>
+475
View File
@@ -0,0 +1,475 @@
<template>
<!-- 角色选择页面 -->
<div class="portal-select">
<!-- 加载中状态 -->
<div v-if="loading" class="loading-container">
<el-icon class="loading-icon" :size="48">
<Loading />
</el-icon>
<p class="loading-text">正在加载用户信息...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<el-icon class="error-icon" :size="48" color="#ef4444">
<CircleCloseFilled />
</el-icon>
<p class="error-text">{{ error }}</p>
<el-button type="primary" @click="handleRetry">重试</el-button>
</div>
<!-- 角色选择页面 -->
<div v-else class="select-container">
<!-- 标题区域 -->
<div class="header">
<h1 class="title">IT智能服务台</h1>
<p class="subtitle">选择您要进入的工作台</p>
</div>
<!-- 用户信息卡片 -->
<div v-if="userInfo" class="user-info">
<el-avatar :size="48" :src="userInfo.avatar || undefined">
{{ userInfo.name?.charAt(0) || '?' }}
</el-avatar>
<div class="user-details">
<span class="user-name">{{ userInfo.name }}</span>
<span v-if="userInfo.department" class="user-department">{{ userInfo.department }}</span>
</div>
</div>
<!-- 角色卡片列表 -->
<div class="role-cards">
<!-- 用户端卡片 -->
<div
class="role-card"
:class="{ 'role-card--active': selectedRole === 'user' }"
@click="selectRole('user')"
>
<div class="role-card__icon">
<el-icon :size="48" color="#3b82f6">
<User />
</el-icon>
</div>
<div class="role-card__content">
<h3 class="role-card__title">用户端</h3>
<p class="role-card__desc">提交工单查看进度浏览知识库</p>
</div>
<div class="role-card__action">
<el-button type="primary" size="large" @click.stop="enterRole('user')">
进入
</el-button>
</div>
</div>
<!-- 坐席端卡片 -->
<div
v-if="hasAgentRole"
class="role-card"
:class="{ 'role-card--active': selectedRole === 'agent' }"
@click="selectRole('agent')"
>
<div class="role-card__icon">
<el-icon :size="48" color="#f59e0b">
<Headset />
</el-icon>
</div>
<div class="role-card__content">
<h3 class="role-card__title">坐席端</h3>
<p class="role-card__desc">处理会话AI辅助管理工单</p>
</div>
<div class="role-card__action">
<el-button type="warning" size="large" @click.stop="enterRole('agent')">
进入
</el-button>
</div>
</div>
<!-- 管理端卡片 -->
<div
v-if="hasAdminRole"
class="role-card"
:class="{ 'role-card--active': selectedRole === 'admin' }"
@click="selectRole('admin')"
>
<div class="role-card__icon">
<el-icon :size="48" color="#ef4444">
<Setting />
</el-icon>
</div>
<div class="role-card__content">
<h3 class="role-card__title">管理端</h3>
<p class="role-card__desc">系统配置数据分析权限管理</p>
</div>
<div class="role-card__action">
<el-button type="danger" size="large" @click.stop="enterRole('admin')">
进入
</el-button>
</div>
</div>
</div>
<!-- 底部提示 -->
<div class="footer">
<p class="footer-text">
当前账号{{ userInfo?.name }} ({{ userInfo?.employee_id }})
</p>
<p class="footer-hint">
<el-icon><InfoFilled /></el-icon>
每次进入需要重新选择工作台
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { usePortalStore } from '@/stores/portal'
import { storeToRefs } from 'pinia'
import { Loading, CircleCloseFilled, User, Headset, Setting, InfoFilled } from '@element-plus/icons-vue'
import apiClient from '@/api/index'
// 获取 Portal Store
const portalStore = usePortalStore()
// 解构状态
const { userInfo, loading, error, hasAgentRole, hasAdminRole } = storeToRefs(portalStore)
// 选中的角色
const selectedRole = ref<string | null>(null)
// ==================== 生命周期 ====================
onMounted(async () => {
const urlParams = new URLSearchParams(window.location.search)
const token = urlParams.get('token')
const code = urlParams.get('code')
// 1. 企微 OAuth2 回调:URL 中有 code 参数
if (code && !token) {
loading.value = true
try {
const response = await apiClient.post('/h5/oauth/callback', { code })
const data = response.data.data
if (data?.token) {
portalStore.setToken(data.token)
// 清除 URL 中的 code/state 参数
window.history.replaceState({}, '', window.location.pathname)
} else {
error.value = 'OAuth2授权失败:未获取到Token'
return
}
} catch (err: any) {
console.error('OAuth2 回调失败:', err)
error.value = err.response?.data?.detail || 'OAuth2授权失败'
return
} finally {
loading.value = false
}
}
// 2. 从 Portal 跳转回来:URL 中有 token 参数
if (token) {
portalStore.setToken(token)
window.history.replaceState({}, '', window.location.pathname)
}
// 3. 检查是否已登录(localStorage 缓存)
if (!portalStore.isAuthenticated) {
portalStore.restoreFromCache()
if (!portalStore.isAuthenticated) {
// 未登录,尝试触发 OAuth2 流程
const corpId = import.meta.env.VITE_WECOM_CORP_ID || ''
if (corpId) {
// 生产环境:调用后端获取 OAuth2 授权 URL
try {
loading.value = true
const response = await apiClient.get('/h5/oauth/authorize')
const authorizeUrl = response.data.data?.authorize_url
if (authorizeUrl) {
window.location.href = authorizeUrl
return
}
} catch (err) {
console.error('获取 OAuth2 URL 失败:', err)
} finally {
loading.value = false
}
}
// 无 corpId 或获取失败:显示错误(开发环境可 Mock 登录)
error.value = '未登录,请通过企业微信工作台访问'
return
}
}
// 4. 加载用户信息
await portalStore.fetchUserInfo()
// 5. 如果用户只有一个有效角色,直接跳转(避免多角色用户被自动跳走)
// 注意:user角色是默认的,但如果有agent或admin角色,应该让用户选择
const validRoles = portalStore.roles.filter(
(r: any) => r.name === 'agent' || r.name === 'admin'
)
if (validRoles.length === 0 && portalStore.roleCount === 1) {
// 只有默认的user角色,直接跳转
const singleRole = portalStore.roles[0]
if (singleRole) {
enterRole(singleRole.name)
}
}
// 否则:显示角色选择页面(让用户选择)
})
// ==================== 方法 ====================
/**
* 选择角色
*/
function selectRole(role: string) {
selectedRole.value = role
}
/**
* 进入角色对应的工作台
*/
function enterRole(role: string) {
// 保存当前选择的角色到 localStorage(用于记住选择)
localStorage.setItem('portal_selected_role', role)
// 跳转到对应的工作台
const roleUrls: Record<string, string> = {
user: '/itdesk/',
agent: '/itagent/',
admin: '/itadmin/',
}
const url = roleUrls[role]
if (url) {
// 将 Token 传递给目标页面
const token = portalStore.token
if (token) {
window.location.href = `${url}?token=${encodeURIComponent(token)}`
} else {
window.location.href = url
}
}
}
/**
* 重试加载
*/
function handleRetry() {
portalStore.fetchUserInfo()
}
</script>
<style scoped>
/* 页面容器 */
.portal-select {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
/* 加载中状态 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.loading-icon {
animation: spin 1s linear infinite;
color: #3b82f6;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loading-text {
color: #94a3b8;
font-size: 16px;
}
/* 错误状态 */
.error-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.error-text {
color: #ef4444;
font-size: 16px;
}
/* 选择页面 */
.select-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 32px;
max-width: 800px;
width: 100%;
}
/* 标题区域 */
.header {
text-align: center;
}
.title {
font-size: 32px;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 8px;
}
.subtitle {
font-size: 16px;
color: #94a3b8;
}
/* 用户信息卡片 */
.user-info {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
background: rgba(30, 41, 59, 0.8);
border-radius: 12px;
border: 1px solid rgba(71, 85, 105, 0.5);
}
.user-details {
display: flex;
flex-direction: column;
}
.user-name {
font-size: 16px;
font-weight: 500;
color: #f1f5f9;
}
.user-department {
font-size: 14px;
color: #94a3b8;
}
/* 角色卡片列表 */
.role-cards {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
}
/* 角色卡片 */
.role-card {
display: flex;
align-items: center;
gap: 20px;
padding: 24px;
background: rgba(30, 41, 59, 0.8);
border-radius: 16px;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.3s ease;
}
.role-card:hover {
background: rgba(30, 41, 59, 0.95);
border-color: rgba(59, 130, 246, 0.5);
transform: translateY(-2px);
}
.role-card--active {
border-color: #3b82f6;
background: rgba(59, 130, 246, 0.1);
}
.role-card__icon {
flex-shrink: 0;
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(59, 130, 246, 0.1);
border-radius: 16px;
}
.role-card__content {
flex: 1;
}
.role-card__title {
font-size: 20px;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 4px;
}
.role-card__desc {
font-size: 14px;
color: #94a3b8;
}
.role-card__action {
flex-shrink: 0;
}
/* 底部提示 */
.footer {
text-align: center;
margin-top: 16px;
}
.footer-text {
font-size: 14px;
color: #64748b;
margin-bottom: 8px;
}
.footer-hint {
font-size: 12px;
color: #475569;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
/* 响应式布局 */
@media (max-width: 640px) {
.role-card {
flex-direction: column;
text-align: center;
}
.role-card__icon {
width: 64px;
height: 64px;
}
.role-card__action {
width: 100%;
}
.role-card__action .el-button {
width: 100%;
}
}
</style>
+31
View File
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Alias */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+43
View File
@@ -0,0 +1,43 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
// 基础路径 -- 部署在 /itportal/ 子路径
base: '/itportal/',
plugins: [vue()],
// 开发服务器配置
server: {
// 开发端口:5176,避免与 agent(5173)、h5(5174)、admin(5175) 冲突
port: 5176,
// 启动后自动打开浏览器
open: true,
// 代理配置:开发环境将 /api 请求代理到后端
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
// 剥离 /api 前缀(生产环境由 Nginx 负责剥离)
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
// 构建配置
build: {
// 输出目录
outDir: 'dist',
// 小于 4KB 的资源内联为 base64
assetsInlineLimit: 4096,
},
// 路径别名
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
})