v2 refacting
This commit is contained in:
+7
-4
@@ -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=
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -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
@@ -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",
|
||||||
|
|||||||
+35
-11
@@ -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,
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 100vh:footer 永远在视口底部,
|
||||||
|
内容少时 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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+41
-5
@@ -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()
|
||||||
}
|
}
|
||||||
|
// 当前域名 → 后端按域名解析 customer(local 自动归 dev,prod 必须 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,16 +30,48 @@ 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)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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('/')
|
||||||
|
|||||||
+49
-31
@@ -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)) {
|
||||||
|
list.forEach((record: Event) => {
|
||||||
eventDataList.push(record)
|
eventDataList.push(record)
|
||||||
eventDataObject[record.reminder_date] = record.title
|
eventDataObject[record.eventDate] = record.title
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// 业务异常已经被 axios 拦截器 toast 了
|
||||||
})
|
})
|
||||||
isComponentActive.value = true
|
|
||||||
isComponentLoading.value = false
|
isComponentLoading.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+67
-4
@@ -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 开关:仅 serve(dev)模式 + .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: {
|
||||||
|
|||||||
@@ -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==
|
||||||
|
|||||||
Reference in New Issue
Block a user