Refactor frontend workspaces and centralize API contracts
Organize domain APIs and types under common/apis, shared components under named folders, and private workspace logic alongside each page. Use Bearer sessions, explicit response envelopes, session-aware routing and streams, and configurable login captcha presentation for the deployed backend contract. Preserve news, economic calendar, crawler, market, chat, Skills, settings, and demo trading workflows with responsive layouts and explicit configuration saves. Validate with frontend lint, type checking, 22 test groups, production builds, and deployed desktop/mobile page checks.
This commit is contained in:
1 parent
77b33e5e77
commit
bc480fb806
102 files changed
+4716
-1707
No files matched your search
@@ -0,0 +1,41 @@
|
||||
import type { Router } from "vue-router"
|
||||
import axios from "axios"
|
||||
import { ApiError } from "@/http/axios"
|
||||
import { useUserStore } from "@/pinia/stores/user"
|
||||
|
||||
export function safeRedirect(value: unknown): string {
|
||||
return typeof value === "string" && value.startsWith("/") && !value.startsWith("//")
|
||||
&& !value.includes("\\") && !/\s/.test(value) && !value.startsWith("/login")
|
||||
? value
|
||||
: "/news"
|
||||
}
|
||||
|
||||
export function installGuards(router: Router) {
|
||||
router.beforeEach(async (to) => {
|
||||
const user = useUserStore()
|
||||
const version = user.sessionVersion
|
||||
if (to.meta.public && to.name !== "login")
|
||||
return true
|
||||
if (!user.token)
|
||||
return to.name === "login" ? true : { name: "login", query: { redirect: to.fullPath } }
|
||||
try {
|
||||
if (!user.identity)
|
||||
await user.fetchMe()
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.code === 401 && !user.token && user.sessionVersion === version + 1)
|
||||
return { name: "login", query: { redirect: to.fullPath } }
|
||||
if (version !== user.sessionVersion || axios.isCancel(error))
|
||||
return false
|
||||
if (error instanceof ApiError && error.code === 401)
|
||||
return { name: "login", query: { redirect: to.fullPath } }
|
||||
return { name: "connection-error", query: { redirect: to.fullPath } }
|
||||
}
|
||||
if (version !== user.sessionVersion)
|
||||
return false
|
||||
if (to.name === "login")
|
||||
return safeRedirect(to.query.redirect)
|
||||
if (to.meta.admin && !user.identity?.is_admin)
|
||||
return { name: "forbidden" }
|
||||
return true
|
||||
})
|
||||
}
|
||||
+8
-25
@@ -1,36 +1,19 @@
|
||||
import Layout from "@@/components/AppLayout/index.vue"
|
||||
import { createRouter, createWebHistory } from "vue-router"
|
||||
import Layout from "@/layouts/index.vue"
|
||||
import { useUserStore } from "@/pinia/user"
|
||||
import { installGuards } from "./guards"
|
||||
|
||||
export const menuRoutes = [
|
||||
{ path: "/news", name: "news", component: () => import("@/pages/news/index.vue"), meta: { title: "新闻流" } },
|
||||
{ path: "/crawler", name: "crawler", component: () => import("@/pages/crawler/index.vue"), meta: { title: "采集控制台" } },
|
||||
{ path: "/market", name: "market", component: () => import("@/pages/market/index.vue"), meta: { title: "行情" } },
|
||||
{ path: "/trading", name: "trading", component: () => import("@/pages/trading/index.vue"), meta: { title: "模拟盘" } },
|
||||
{ path: "/chat", name: "chat", component: () => import("@/pages/chat/index.vue"), meta: { title: "Chat" } },
|
||||
{ path: "/skills", name: "skills", component: () => import("@/pages/skills/index.vue"), meta: { title: "Skills" } },
|
||||
{ path: "/settings", name: "settings", component: () => import("@/pages/settings/index.vue"), meta: { title: "环境变量" } }
|
||||
]
|
||||
import { menuRoutes } from "./routes"
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: "/login", name: "login", component: () => import("@/pages/login/index.vue") },
|
||||
{ path: "/", component: Layout, redirect: "/news", children: menuRoutes },
|
||||
{ path: "/:pathMatch(.*)*", redirect: "/" }
|
||||
{ path: "/docs", name: "docs", component: () => import("@/pages/docs/index.vue"), meta: { admin: true } },
|
||||
{ path: "/403", name: "forbidden", component: () => import("@/pages/error/index.vue"), meta: { status: 403 } },
|
||||
{ path: "/connection-error", name: "connection-error", component: () => import("@/pages/error/index.vue"), meta: { public: true, status: 503 } },
|
||||
{ path: "/:pathMatch(.*)*", name: "not-found", component: () => import("@/pages/error/index.vue"), meta: { status: 404 } }
|
||||
]
|
||||
})
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
if (to.name === "login")
|
||||
return true
|
||||
const userStore = useUserStore()
|
||||
if (!userStore.username) {
|
||||
try {
|
||||
await userStore.fetchMe()
|
||||
} catch {
|
||||
return { name: "login", query: { redirect: to.fullPath } }
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
installGuards(router)
|
||||
@@ -0,0 +1,10 @@
|
||||
export const menuRoutes = [
|
||||
{ path: "/news", name: "news", component: () => import("@/pages/news/index.vue"), meta: { admin: true, title: "新闻流" } },
|
||||
{ path: "/calendar", name: "calendar", component: () => import("@/pages/calendar/index.vue"), meta: { admin: true, title: "日历" } },
|
||||
{ path: "/crawler", name: "crawler", component: () => import("@/pages/crawler/index.vue"), meta: { admin: true, title: "采集控制台" } },
|
||||
{ path: "/market", name: "market", component: () => import("@/pages/market/index.vue"), meta: { admin: true, title: "行情" } },
|
||||
{ path: "/trading", name: "trading", component: () => import("@/pages/trading/index.vue"), meta: { admin: true, title: "模拟盘" } },
|
||||
{ path: "/chat", name: "chat", component: () => import("@/pages/chat/index.vue"), meta: { admin: true, title: "Chat" } },
|
||||
{ path: "/skills", name: "skills", component: () => import("@/pages/skills/index.vue"), meta: { admin: true, title: "Skills" } },
|
||||
{ path: "/settings", name: "settings", component: () => import("@/pages/settings/index.vue"), meta: { admin: true, title: "环境变量" } }
|
||||
]
|
||||
Reference in new issue
Block a user