v2 refacting

This commit is contained in:
simple321vip
2026-06-28 23:02:32 +08:00
parent 4da6832b44
commit 024f306636
14 changed files with 360 additions and 90 deletions
+7 -4
View File
@@ -6,12 +6,15 @@
VITE_MODE_NAME=development VITE_MODE_NAME=development
# 资源 CDN 前缀 # 资源 CDN 前缀
VITE_RES_URL=http://localhost:3000/ VITE_RES_URL=http://localhost:5173/
# 后端 API 基础地址 # 后端 API 基础地址
# dev: http://localhost:8080/auth # ⚠️ 本地必须写前端自己(http://localhost:5173),由 Vite proxy 转发
# 写 http://localhost:8080/auth 会让浏览器绕过 proxy 直连 8080
# dev: http://localhost:5173
# dev (K8s): https://dev.violin-home.cn
# prod: https://www.violin-home.cn # prod: https://www.violin-home.cn
VITE_APP_URL=http://localhost:8080/auth VITE_APP_URL=http://localhost:5173
# 应用元信息 # 应用元信息
VITE_APP_TITLE=violin-home VITE_APP_TITLE=violin-home
@@ -35,7 +38,7 @@ VITE_OIDC_ISSUER=https://auth.violin-work.online
VITE_OIDC_CLIENT_ID= VITE_OIDC_CLIENT_ID=
# 回调 URI(必须与 Authentik 后台配置的完全一致) # 回调 URI(必须与 Authentik 后台配置的完全一致)
# dev: http://localhost:3000/auth/callback # dev: http://localhost:5173/auth/callback
# prod: https://www.violin-home.cn/auth/callback # prod: https://www.violin-home.cn/auth/callback
VITE_OIDC_REDIRECT_URI= VITE_OIDC_REDIRECT_URI=
+1 -1
View File
@@ -14,7 +14,7 @@
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"element-plus": "^2.8.4", "element-plus": "^2.14.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"md-editor-v3": "^4.20.1", "md-editor-v3": "^4.20.1",
+1 -1
View File
@@ -16,7 +16,7 @@
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"element-plus": "^2.8.4", "element-plus": "^2.14.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"md-editor-v3": "^4.20.1", "md-editor-v3": "^4.20.1",
+36 -12
View File
@@ -1,26 +1,50 @@
import { service } from '../utils/request' import { service } from '../utils/request'
const headers = { /**
'Content-Type': 'application/json;charsetset=UTF-8' * 日历事件 API
} * 对应后端 CalendarController: /api/v1/events
*/
const put_event = (data: Object) => { const post_event = (data: {
eventDate: string
title: string
eventInfo?: string
}) => {
return service({ return service({
url: '/calendar/api/v1/event', url: '/calendar/api/v1/events',
method: 'PUT', method: 'POST',
data: data data
}) })
} }
const get_event = () => { const get_event = () => {
return service({ return service({
url: '/calendar/api/v1/event', url: '/calendar/api/v1/events',
method: 'GET', method: 'GET'
headers: headers })
}
const put_event = (id: number, data: {
eventDate: string
title: string
eventInfo?: string
}) => {
return service({
url: `/calendar/api/v1/events/${id}`,
method: 'PUT',
data
})
}
const delete_event = (id: number) => {
return service({
url: `/calendar/api/v1/events/${id}`,
method: 'DELETE'
}) })
} }
export { export {
get_event, get_event,
post_event,
put_event, put_event,
} delete_event,
}
+54 -12
View File
@@ -12,12 +12,14 @@
<MainApp></MainApp> <MainApp></MainApp>
</el-main> </el-main>
<el-footer> <el-footer>
<span> <div class="footer-left">
管祥玮的个人网站 ©Copyright 2022-2022 <span>{{ SITE_NAME }} {{ COPYRIGHT_TEXT }}</span>
</span> <span class="separator">|</span>
<span @click="openSiteQuery">辽ICP备2022003637号-2</span> <span class="icp-link" @click="openSiteQuery">{{ ICP_NUMBER }}</span>
<div> </div>
<el-avatar @click="openGithub" src="https://github.githubassets.com/favicons/favicon.svg" /> <div class="footer-right">
<el-avatar :size="24" class="github-icon" @click="openGithub"
src="https://github.githubassets.com/favicons/favicon.svg" />
</div> </div>
</el-footer> </el-footer>
</el-container> </el-container>
@@ -30,6 +32,7 @@ import MainApp from '../layout/MainApp.vue'
import NavBar from "../layout/NavBar.vue" import NavBar from "../layout/NavBar.vue"
import SideBar from "../layout/SideBar.vue" import SideBar from "../layout/SideBar.vue"
import { settingsStore } from "@/store/modules/settings" import { settingsStore } from "@/store/modules/settings"
import { SITE_NAME, COPYRIGHT_TEXT, ICP_NUMBER, ICP_QUERY_URL } from "@/const/site"
// -- IMPORT -- // -- IMPORT --
const useSettingsStore = settingsStore() const useSettingsStore = settingsStore()
@@ -62,8 +65,7 @@ const openGithub = () => {
} }
const openSiteQuery = () => { const openSiteQuery = () => {
let href = 'https://beian.miit.gov.cn/#/Integrated/index' window.open(ICP_QUERY_URL, '_blank')
window.open(href, '_blank')
} }
/** /**
@@ -81,9 +83,12 @@ iniPage()
} }
.el-main { .el-main {
/* 让 main 区占满 header 与 footer 之间的剩余空间,
内容多了 main 内部滚动,footer 永远钉在 layout 底部 */
padding: 16px; padding: 16px;
overflow: hidden; flex: 1;
height: 100%; min-height: 0; /* flex item 默认 min-height: auto 会阻止收缩 */
overflow-y: auto;
} }
.el-aside { .el-aside {
@@ -93,13 +98,50 @@ iniPage()
} }
.el-footer { .el-footer {
/* flex: 0 0 auto 让 footer 保持自身高度(不撑开、不收缩) */
flex: 0 0 auto;
margin-top: 20px; margin-top: 20px;
display: flex; display: flex;
justify-content: space-around; justify-content: space-between;
align-items: center;
padding: 12px 24px;
border-top: 1px solid rgba(151, 151, 151, 0.2);
font-size: 16px;
color: #666;
}
.footer-left {
display: flex;
align-items: center;
gap: 8px;
}
.footer-right {
display: flex;
align-items: center;
}
.separator {
color: #ccc;
}
.icp-link {
cursor: pointer;
user-select: none;
}
.icp-link:hover {
color: #409eff;
}
.github-icon {
/* 跟 footer 文字 baseline 对齐 */
vertical-align: middle;
cursor: pointer;
} }
.main_container { .main_container {
/* height: 100%; */ height: 100%;
} }
.homeWrap { .homeWrap {
+63 -6
View File
@@ -18,10 +18,15 @@
</div> </div>
</el-card> </el-card>
<div class="footer"> <div class="footer">
<span> <div class="footer-left">
管祥玮的个人网站 ©Copyright 2022-2022 <span>{{ SITE_NAME }} {{ COPYRIGHT_TEXT }}</span>
</span> <span class="separator">|</span>
<span @click="openSiteQuery">辽ICP备2022003637号-2</span> <span class="icp-link" @click="openSiteQuery">{{ ICP_NUMBER }}</span>
</div>
<div class="footer-right">
<el-avatar :size="24" class="github-icon" @click="openGithub"
src="https://github.githubassets.com/favicons/favicon.svg" />
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -29,6 +34,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { reactive } from 'vue' import { reactive } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { SITE_NAME, COPYRIGHT_TEXT, ICP_NUMBER, ICP_QUERY_URL } from '@/const/site'
const handleOidcLogin = () => { const handleOidcLogin = () => {
const issuer = import.meta.env.VITE_OIDC_ISSUER const issuer = import.meta.env.VITE_OIDC_ISSUER
@@ -49,8 +55,11 @@ const handleOidcLogin = () => {
} }
const openSiteQuery = () => { const openSiteQuery = () => {
let href = 'https://beian.miit.gov.cn/#/Integrated/index' window.open(ICP_QUERY_URL, '_blank')
window.open(href, '_blank') }
const openGithub = () => {
window.open('https://github.com/simple321vip', '_blank')
} }
const style = reactive({} as any) const style = reactive({} as any)
@@ -67,6 +76,9 @@ style.width = window.innerWidth * 0.7 + 'px'
} }
.ttt { .ttt {
/* flex column + min-height 100vhfooter 永远在视口底部,
内容少时 footer 贴底,内容多时 footer 在内容之后但不漂 */
min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -79,4 +91,49 @@ style.width = window.innerWidth * 0.7 + 'px'
background: url('../../assets/have_a_nice_day.jpeg') center top no-repeat; background: url('../../assets/have_a_nice_day.jpeg') center top no-repeat;
background-size: cover; background-size: cover;
} }
.box-card {
/* 中间内容区撑开剩余空间 */
flex: 1;
}
.footer {
flex: 0 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 24px;
border-top: 1px solid rgba(151, 151, 151, 0.2);
font-size: 16px;
color: #666;
}
.footer-left {
display: flex;
align-items: center;
gap: 8px;
}
.footer-right {
display: flex;
align-items: center;
}
.separator {
color: #ccc;
}
.icp-link {
cursor: pointer;
user-select: none;
}
.icp-link:hover {
color: #409eff;
}
.github-icon {
vertical-align: middle;
cursor: pointer;
}
</style> </style>
+20
View File
@@ -0,0 +1,20 @@
/**
* 站点元信息常量。
*
* <p>⚠️ ICP 备案号是公网安部要求强制展示的,<b>不能修改</b>。文案变更需重新备案。</p>
*/
/** 产品名(footer / 浏览器 title / 侧边栏使用) */
export const SITE_NAME = 'Violin Home'
/** 版权年份区间(自动取当前年) */
export const COPYRIGHT_YEARS = `2022-${new Date().getFullYear()}`
/** 完整版权字符串(footer 用) */
export const COPYRIGHT_TEXT = `©Copyright ${COPYRIGHT_YEARS}`
/** ICP 备案号(⚠️ 公网安部要求原样显示,不可修改) */
export const ICP_NUMBER = '辽ICP备2022003637号-2'
/** ICP 备案查询 URL(公网安部要求链接到工信部查询页) */
export const ICP_QUERY_URL = 'https://beian.miit.gov.cn/#/Integrated/index'
+8 -4
View File
@@ -19,11 +19,15 @@ type DataTimeline = {
message: string, message: string,
} }
// 与后端 CalendarEventResponse 对齐(camelCase
type Event = { type Event = {
reminder_date: string, id?: number
title: string, customerId?: string
info: string, eventDate: string
type: string[], title: string
eventInfo?: string
createdAt?: string
updatedAt?: string
} }
interface Wiki { interface Wiki {
+2 -2
View File
@@ -1,4 +1,4 @@
import { createRouter, createMemoryHistory, RouteRecordRaw } from 'vue-router' import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
const login = () => import('@/components/login/login.vue') const login = () => import('@/components/login/login.vue')
const home = () => import('@/view/dashboard/index.vue') const home = () => import('@/view/dashboard/index.vue')
@@ -134,7 +134,7 @@ const routes: Array<RouteRecordRaw> = [
const router = createRouter({ const router = createRouter({
history: createMemoryHistory(), history: createWebHistory(),
routes routes
}) })
+42 -6
View File
@@ -1,8 +1,8 @@
import axios from 'axios' import axios from 'axios'
import router from '../router' import router from '../router'
import { getToken, getCustomer, resetToken } from '../utils/auth' import { getToken, getCustomer, resetToken } from '../utils/auth'
import { ElMessage } from 'element-plus'
// violin-wiki
const service = axios.create( const service = axios.create(
{ {
baseURL: import.meta.env.VITE_APP_URL as string, baseURL: import.meta.env.VITE_APP_URL as string,
@@ -13,8 +13,12 @@ const service = axios.create(
service.interceptors.request.use( service.interceptors.request.use(
config => { config => {
if (typeof getToken() === 'string') { if (typeof getToken() === 'string') {
(config as any).headers['Authorization'] = 'Bearer:' + getToken() // ⚠️ Bearer 后必须有空格,否则后端 JWT 解析器会识别失败 → 401
(config as any).headers['Authorization'] = 'Bearer ' + getToken()
} }
// 当前域名 → 后端按域名解析 customerlocal 自动归 devprod 必须 admin 预先注册)
;(config as any).headers['X-Customer-Domain'] = window.location.hostname
const customer = getCustomer() const customer = getCustomer()
if (typeof customer === 'string') { if (typeof customer === 'string') {
(config as any).headers['customerId'] = JSON.parse(customer).customerId (config as any).headers['customerId'] = JSON.parse(customer).customerId
@@ -26,17 +30,49 @@ service.interceptors.request.use(
service.interceptors.response.use( service.interceptors.response.use(
(response) => { (response) => {
const data = response.data
// 后端 ApiResponse / ErrorResponse 结构:{ code, message, ... }
// 业务 code 非 0 / AUTH_401 等都按错误处理
if (data && typeof data === 'object' && 'code' in data) {
if (data.code === 0 || data.code === '0' || data.code === 200) {
return response
}
// 401 业务码:跳登录
if (data.code === 'AUTH_401' || data.code === 'AUTH_CONTEXT_MISSING') {
ElMessage.warning('登录已过期,请重新登录')
resetToken()
router.replace('/login').catch(() => {})
return Promise.reject(new Error(data.message || '未登录'))
}
// 其他业务错误:弹错
ElMessage.error(data.message || `请求失败 (${data.code})`)
return Promise.reject(new Error(data.message || '请求失败'))
}
return response return response
}, },
(error) => { (error) => {
if (error && error.response) { if (error && error.response) {
if (error.response.status == 401) { const status = error.response.status
const body = error.response.data
if (status === 401) {
// HTTP 401:登录失效
const msg = (body && body.message) || '登录已过期,请重新登录'
ElMessage.warning(msg)
resetToken() resetToken()
router.resolve({ path: '/' }); router.replace('/login').catch(() => {})
return } else if (status >= 500) {
ElMessage.error((body && body.message) || '服务器内部错误')
} else if (status >= 400) {
ElMessage.error((body && body.message) || `请求错误 (${status})`)
} else {
ElMessage.error('网络异常')
} }
} else {
// 无 response:网络层 / CORS / 超时
ElMessage.error(error.message || '网络异常')
} }
return Promise.reject(error)
} }
) )
export { service } export { service }
+6 -3
View File
@@ -34,10 +34,13 @@ onMounted(async () => {
} }
}) })
setToken(res.data.token) // 后端 ApiResponse 结构:{ code, message, data: { token, sub, username, ... } }
// 业务字段在 data.data 下
const payload = (res.data && res.data.data) || res.data
setToken(payload.token)
setCustomer({ setCustomer({
customerId: res.data.sub, customerId: payload.customerId || payload.sub,
account: res.data.username account: payload.username
}) })
sessionStorage.removeItem('oidc_state') sessionStorage.removeItem('oidc_state')
router.push('/') router.push('/')
+51 -33
View File
@@ -1,44 +1,47 @@
<template> <template>
<el-calendar v-model="value" v-if="isComponentActive" v-loading="isComponentLoading" <el-calendar v-model="value" v-if="isComponentActive" v-loading="isComponentLoading"
element-loading-text="主人别着急我在努力加载中^_^"> element-loading-text="主人别着急我在努力加载中^_^">
<template #dateCell="{ data }" lo> <template #date-cell="{ data }">
<div class="date-cell-holiday" v-if="Object.keys(holidays).includes(toLocalDate(data.date))"> <div class="date-cell-holiday"
<span style="color: red;" @click="onclick(data.date)"> {{ data.date.getDate() }}</span><br> v-if="Object.keys(holidays).includes(data.day)"
<span>{{ holidays[toLocalDate(data.date)] }}</span> @click="onclick(data.date)"
style="cursor: pointer;">
<span style="color: red;">{{ data.day.split('-')[2] }}</span><br>
<span>{{ holidays[data.day] }}</span>
</div> </div>
<div class="date-cell" v-else> <div class="date-cell" v-else @click="onclick(data.date)" style="cursor: pointer;">
<span style="color: red;" v-if="data.date.getDay() == 0 || data.date.getDay() == 6" @click="onclick(data.date)"> <span style="color: red;" v-if="data.date.getDay() == 0 || data.date.getDay() == 6">
{{ {{ data.day.split('-')[2] }}
data.date.getDate() </span>
}}</span> <span v-else> {{ data.day.split('-')[2] }}</span>
<span v-else @click="onclick(data.date)"> {{ data.date.getDate() }}</span>
</div> </div>
<div class="date-cell-event" v-if="Object.keys(eventDataObject).includes(toLocalDate(data.date))"> <div class="date-cell-event" v-if="Object.keys(eventDataObject).includes(data.day)">
<span>{{ eventDataObject[toLocalDate(data.date)] }}</span> <span>{{ eventDataObject[data.day] }}</span>
</div> </div>
</template> </template>
</el-calendar> </el-calendar>
<span>待办事项</span> <span>待办事项</span>
<el-table :data="eventDataList"> <el-table :data="eventDataList">
<el-table-column property="reminder_date" label="Date" width="150" /> <el-table-column property="eventDate" label="Date" width="150" />
<el-table-column property="title" label="title" width="200" /> <el-table-column property="title" label="title" width="200" />
<el-table-column property="reminder_info" label="eventInfo" /> <el-table-column property="eventInfo" label="eventInfo" />
</el-table> </el-table>
<el-dialog v-model="dialogFormVisible" title="待办事项"> <el-dialog v-model="dialogFormVisible" title="待办事项">
<el-form :model="form"> <el-form :model="form">
<el-form-item label="日期" :label-width="formLabelWidth"> <el-form-item label="日期" :label-width="formLabelWidth">
<span>{{ form.reminder_date }}</span> <span>{{ form.eventDate }}</span>
</el-form-item> </el-form-item>
<el-form-item label="title" :label-width="formLabelWidth"> <el-form-item label="title" :label-width="formLabelWidth">
<el-input v-model="form.title" autocomplete="off" /> <el-input v-model="form.title" autocomplete="off" />
</el-form-item> </el-form-item>
<el-form-item label="eventInfo" :label-width="formLabelWidth"> <el-form-item label="eventInfo" :label-width="formLabelWidth">
<el-input v-model="form.reminder_info" autocomplete="off" type="textarea" /> <el-input v-model="form.eventInfo" autocomplete="off" type="textarea" />
</el-form-item> </el-form-item>
<el-form-item label="alert" :label-width="formLabelWidth"> <el-form-item label="alert" :label-width="formLabelWidth">
<el-checkbox-group v-model="checkedalerts" :min="0" :max="2"> <el-checkbox-group v-model="checkedalerts" :min="0" :max="2">
<el-checkbox v-for="city in alerts" :key="city" :label="city">{{ <el-checkbox v-for="city in alerts" :key="city" :value="city">{{
city city
}}</el-checkbox> }}</el-checkbox>
</el-checkbox-group> </el-checkbox-group>
@@ -59,24 +62,24 @@
import { nextTick, onMounted, reactive, ref } from 'vue' import { nextTick, onMounted, reactive, ref } from 'vue'
import { get_holiday } from '@/service/calendar' import { get_holiday } from '@/service/calendar'
import { toLocalDate } from '@/utils/date' import { toLocalDate } from '@/utils/date'
import { get_event, put_event } from "@/api/calendar" import { get_event, post_event, put_event, delete_event } from '@/api/calendar'
import { Event } from "@/entity/index" import { Event } from '@/entity/index'
import { settingsStore } from "@/store/modules/settings"; import { settingsStore } from '@/store/modules/settings'
// -- IMPORT -- // -- IMPORT --
const useSettingsStore = settingsStore() const useSettingsStore = settingsStore()
// -- REACTIVE OBJECT -- // -- REACTIVE OBJECT --
const holidays = reactive<any>({}) const holidays = reactive<Record<string, string>>({})
const eventDataList = reactive<Event[]>([]) const eventDataList = reactive<Event[]>([])
const eventDataObject = reactive<any>({}) const eventDataObject = reactive<Record<string, string>>({})
const form = reactive({ const form = reactive({
reminder_date: '', // 与后端 CalendarEventRequest 对齐(camelCase
eventDate: '',
title: '', title: '',
reminder_info: '', eventInfo: '',
}) })
// -- REF OBJECT -- // -- REF OBJECT --
const value = ref(new Date()) const value = ref(new Date())
@@ -90,13 +93,15 @@ const alerts = ['email', 'messageBox', '手机短信', '微信通知']
// -- EVENT DEFINITION // -- EVENT DEFINITION
const onclick = (date: Date) => { const onclick = (date: Date) => {
form.reminder_date = toLocalDate(date) form.eventDate = toLocalDate(date)
dialogFormVisible.value = true dialogFormVisible.value = true
} }
const confirm = async () => { const confirm = async () => {
await put_event(form).then(() => { await post_event({
eventDate: form.eventDate,
title: form.title,
eventInfo: form.eventInfo,
}) })
dialogFormVisible.value = false dialogFormVisible.value = false
handleCommand('refresh') handleCommand('refresh')
@@ -121,19 +126,32 @@ const handleCommand = (command: string) => {
* AUTO INVOKE FUNCTION * AUTO INVOKE FUNCTION
*/ */
onMounted(async () => { onMounted(async () => {
// 先显示日历(避免数据加载失败阻塞日历渲染)
isComponentActive.value = true
await get_holiday().then((resp) => { await get_holiday().then((resp) => {
// holidays-jp.github.io 直接返回 JSON 对象,不是 ApiResponse 结构
Object.keys(resp.data).forEach(element => { Object.keys(resp.data).forEach(element => {
holidays[element] = resp.data[element] holidays[element] = resp.data[element]
}) })
}).catch(() => {
// 节假日数据失败不影响日历显示
}) })
await get_event().then((response) => { await get_event().then((response) => {
// 后端 ApiResponse 结构:{code, message, data: [...]}
const list = response.data?.data ?? response.data
eventDataList.length = 0 eventDataList.length = 0
response.data.forEach((record: Event) => { if (Array.isArray(list)) {
eventDataList.push(record) list.forEach((record: Event) => {
eventDataObject[record.reminder_date] = record.title eventDataList.push(record)
}) eventDataObject[record.eventDate] = record.title
})
}
}).catch(() => {
// 业务异常已经被 axios 拦截器 toast 了
}) })
isComponentActive.value = true
isComponentLoading.value = false isComponentLoading.value = false
}) })
+68 -5
View File
@@ -7,16 +7,19 @@ import { fileURLToPath, URL } from 'url'
// https://vitejs.dev/guide/env-and-mode.html#env-files // https://vitejs.dev/guide/env-and-mode.html#env-files
export default defineConfig(({ mode, command }) => { export default defineConfig(({ mode, command }) => {
const env = loadEnv(mode, process.cwd()) const env = loadEnv(mode, process.cwd())
// mock 开关:仅 servedev)模式 + .env.local 里 VITE_USE_MOCK=true
// 关掉时直接不注册 viteMockServe 插件(避免 mock routes 被注册)
const useMock = command === 'serve' && env.VITE_USE_MOCK === 'true'
return { return {
plugins: [ plugins: [
vue(), vue(),
// gzip 压缩 // gzip 压缩
viteCompression(), viteCompression(),
// 本地 mock 服务(仅 dev 启用,prod 不引入) // 本地 mock 服务(由 VITE_USE_MOCK 控制;prod / 关 mock 时不引入)
viteMockServe({ ...(useMock ? [viteMockServe({
mockPath: 'mock', mockPath: 'mock',
localEnabled: command === 'serve', localEnabled: true,
}), })] : []),
], ],
base: env.VITE_RES_URL, base: env.VITE_RES_URL,
resolve: { resolve: {
@@ -31,6 +34,66 @@ export default defineConfig(({ mode, command }) => {
} }
} }
}, },
// ============================================================
// dev server proxy —— 本地多后端联调
// ============================================================
// ⚠️ 关键:proxy key 必须用 /api/ 后缀,避免拦截 SPA 路由
// 例如 /bookmark/api/v1/... → 8086(后端 API
// 但 /bookmark → 保留在前端(SPA 路由,由 history fallback 处理)
//
// 调试建议:浏览器开 Network,看 Server 响应头确认走了哪个后端
//
// 端口对齐:
// violin-auth: 8080 (context-path: /auth)
// violin-calendar: 8085 (context-path: /calendar)
// violin-bookmark: 8086 (context-path: /bookmark, 待你确认)
// violin-onenote: 8087 (context-path: /onenote, 待你确认)
// ... 新增服务时在这里加一行
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
// auth: 业务 API 在 /api/v1/auth/...,没有 /auth/api 前缀
// 用 rewrite 把 /api/v1/auth/* 转成 /auth/api/v1/auth/*
// 后端 context-path 是 /auth,所以请求路径需要带 /auth 前缀
'^/api/v1/auth/.*': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/v1\/auth/, '/auth/api/v1/auth'),
},
// 其他服务:path 自带服务名前缀(/calendar/api/...
'^/calendar/api/.*': {
target: 'http://localhost:8085',
changeOrigin: true,
},
'^/bookmark/api/.*': {
target: 'http://localhost:8086',
changeOrigin: true,
},
'^/onenote/api/.*': {
target: 'http://localhost:8087',
changeOrigin: true,
},
'^/trader/api/.*': {
target: 'http://localhost:8088',
changeOrigin: true,
},
'^/traderCalender/api/.*': {
target: 'http://localhost:8088',
changeOrigin: true,
},
'^/cloud/api/.*': {
target: 'http://localhost:8089',
changeOrigin: true,
},
'^/wiki/api/.*': {
target: 'http://localhost:8090',
changeOrigin: true,
},
},
},
build: { build: {
// 拆 chunk,减少首屏加载 // 拆 chunk,减少首屏加载
rollupOptions: { rollupOptions: {
@@ -44,4 +107,4 @@ export default defineConfig(({ mode, command }) => {
} }
} }
} }
}) })
+1 -1
View File
@@ -1114,7 +1114,7 @@ ee-first@1.1.1:
resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
element-plus@^2.8.4: element-plus@^2.14.1:
version "2.14.2" version "2.14.2"
resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.2.tgz" resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.2.tgz"
integrity sha512-eNH9uP3wQoNqieEIHXiNvIVv+zO5sZDU0CAZq5b0zqSN06DD0/V9xIq1R/qm3rw5k3nBTM1JvpxhCfRbaFLzDQ== integrity sha512-eNH9uP3wQoNqieEIHXiNvIVv+zO5sZDU0CAZq5b0zqSN06DD0/V9xIq1R/qm3rw5k3nBTM1JvpxhCfRbaFLzDQ==