Browse Source

fix: 优化全局路径

周玉环 1 month ago
parent
commit
95c4f1ed29

+ 302 - 0
docs/LOCAL_TEST_GUIDE.md

@@ -0,0 +1,302 @@
+# 本地测试指南 - localStorage 共享
+
+## 问题
+
+本地开发时,React 项目和 JeecgBoot 运行在不同端口:
+- React: `http://localhost:3000`
+- JeecgBoot: `http://localhost:3100`
+
+**不同端口 = 不同域名 = localStorage 不共享**
+
+## 解决方案
+
+### 方案 1:Nginx 本地代理(推荐)✅
+
+通过 Nginx 将两个项目统一到一个端口。
+
+#### 步骤 1:安装 Nginx
+
+**macOS:**
+```bash
+brew install nginx
+```
+
+**Windows:**
+下载并安装:https://nginx.org/en/download.html
+
+**Linux:**
+```bash
+sudo apt-get install nginx
+```
+
+#### 步骤 2:配置 Nginx
+
+使用项目根目录的 `nginx.local.conf` 文件:
+
+```bash
+# macOS/Linux
+sudo cp nginx.local.conf /usr/local/etc/nginx/servers/local-dev.conf
+
+# 或者直接编辑 nginx.conf
+sudo nano /usr/local/etc/nginx/nginx.conf
+```
+
+#### 步骤 3:启动服务
+
+```bash
+# 1. 启动 React 项目(端口 3000)
+npm run dev
+
+# 2. 启动 JeecgBoot 前端(端口 3100)
+cd /path/to/jeecg-boot-frontend
+npm run serve
+
+# 3. 启动 Nginx
+sudo nginx
+# 或重启
+sudo nginx -s reload
+```
+
+#### 步骤 4:访问
+
+现在所有服务都通过 `http://localhost:8080` 访问:
+
+- React 项目:`http://localhost:8080/spaces/`
+- JeecgBoot:`http://localhost:8080/system/user`
+
+✅ **localStorage 自动共享!**
+
+#### 停止 Nginx
+
+```bash
+sudo nginx -s stop
+```
+
+---
+
+### 方案 2:修改 hosts + 反向代理
+
+使用自定义域名 + Nginx。
+
+#### 步骤 1:修改 hosts
+
+```bash
+sudo nano /etc/hosts
+```
+
+添加:
+```
+127.0.0.1 local-dev.com
+```
+
+#### 步骤 2:配置 Nginx
+
+```nginx
+server {
+    listen 80;
+    server_name local-dev.com;
+
+    location /spaces/ {
+        proxy_pass http://localhost:3000/spaces/;
+    }
+
+    location / {
+        proxy_pass http://localhost:3100/;
+    }
+}
+```
+
+#### 步骤 3:访问
+
+- React: `http://local-dev.com/spaces/`
+- JeecgBoot: `http://local-dev.com/system/user`
+
+---
+
+### 方案 3:URL 参数传递(临时方案)
+
+**仅用于快速测试,不推荐长期使用。**
+
+#### React 端
+
+跳转时通过 URL 参数传递 token(已在 `jeecgNavigation.ts` 中实现):
+
+```typescript
+// 开发环境自动添加 token 参数
+navigateToJeecg('/system/user');
+// 实际跳转:http://localhost:3100/system/user?_dev_token=xxx
+```
+
+#### JeecgBoot 端
+
+需要在 JeecgBoot 中添加代码读取 URL 参数并写入 localStorage:
+
+```javascript
+// JeecgBoot: src/main.js 或 App.vue
+if (process.env.NODE_ENV === 'development') {
+  const urlParams = new URLSearchParams(window.location.search);
+  const devToken = urlParams.get('_dev_token');
+  
+  if (devToken) {
+    try {
+      localStorage.setItem(
+        '内容中心管理系统__PRODUCTION__1.0.0__COMMON__LOCAL__KEY__',
+        devToken
+      );
+      console.log('[Dev] Token synced from URL parameter');
+      
+      // 移除 URL 参数
+      window.history.replaceState({}, '', window.location.pathname);
+    } catch (e) {
+      console.error('[Dev] Failed to sync token:', e);
+    }
+  }
+}
+```
+
+---
+
+### 方案 4:浏览器插件同步
+
+使用浏览器插件在不同端口间同步 localStorage。
+
+#### Chrome 插件:Storage Area Explorer
+
+1. 安装插件
+2. 手动复制 localStorage 数据
+3. 在另一个端口粘贴
+
+**缺点:** 手动操作,不方便
+
+---
+
+## 推荐方案对比
+
+| 方案 | 优点 | 缺点 | 推荐度 |
+|------|------|------|--------|
+| Nginx 代理 | 完全模拟生产环境,自动共享 | 需要安装 Nginx | ⭐⭐⭐⭐⭐ |
+| hosts + Nginx | 使用自定义域名,更真实 | 配置稍复杂 | ⭐⭐⭐⭐ |
+| URL 参数 | 快速测试,无需额外配置 | 需要修改 JeecgBoot 代码 | ⭐⭐⭐ |
+| 浏览器插件 | 无需代码修改 | 手动操作,不方便 | ⭐⭐ |
+
+## 最佳实践
+
+### 开发环境
+
+使用 **Nginx 本地代理**:
+```
+http://localhost:8080/spaces/     → React
+http://localhost:8080/system/user → JeecgBoot
+```
+
+### 测试环境
+
+使用 **自定义域名**:
+```
+http://test.your-domain.com/spaces/     → React
+http://test.your-domain.com/system/user → JeecgBoot
+```
+
+### 生产环境
+
+使用 **同一域名**:
+```
+https://your-domain.com/spaces/     → React
+https://your-domain.com/system/user → JeecgBoot
+```
+
+## 快速开始(Nginx 方案)
+
+```bash
+# 1. 安装 Nginx
+brew install nginx  # macOS
+# 或
+sudo apt-get install nginx  # Linux
+
+# 2. 复制配置文件
+sudo cp nginx.local.conf /usr/local/etc/nginx/servers/local-dev.conf
+
+# 3. 启动 React(端口 3000)
+npm run dev
+
+# 4. 启动 JeecgBoot(端口 3100)
+cd /path/to/jeecg-boot-frontend
+npm run serve
+
+# 5. 启动 Nginx
+sudo nginx
+
+# 6. 访问测试
+open http://localhost:8080/spaces/
+```
+
+## 验证 localStorage 共享
+
+### 步骤 1:在 React 项目登录
+
+访问:`http://localhost:8080/spaces/login`
+
+### 步骤 2:检查 localStorage
+
+打开浏览器控制台:
+```javascript
+localStorage.getItem('内容中心管理系统__PRODUCTION__1.0.0__COMMON__LOCAL__KEY__')
+```
+
+应该能看到 token 数据。
+
+### 步骤 3:跳转到 JeecgBoot
+
+点击跳转按钮,或直接访问:
+```
+http://localhost:8080/system/user
+```
+
+### 步骤 4:验证免登录
+
+如果 JeecgBoot 页面直接显示内容(没有跳转到登录页),说明 localStorage 共享成功!✅
+
+## 故障排查
+
+### 问题 1:Nginx 启动失败
+
+```bash
+# 检查配置文件语法
+sudo nginx -t
+
+# 查看错误日志
+tail -f /usr/local/var/log/nginx/error.log
+```
+
+### 问题 2:端口被占用
+
+```bash
+# 查看端口占用
+lsof -i :8080
+
+# 杀死进程
+kill -9 <PID>
+```
+
+### 问题 3:代理不生效
+
+检查 Nginx 配置中的端口是否正确:
+```nginx
+proxy_pass http://localhost:3000/;  # React 端口
+proxy_pass http://localhost:3100/;  # JeecgBoot 端口
+```
+
+### 问题 4:localStorage 还是不共享
+
+1. 确认访问的是 `http://localhost:8080`(不是 3000 或 3100)
+2. 清除浏览器缓存
+3. 检查 Nginx 日志
+
+## 总结
+
+**本地测试 localStorage 共享的关键:**
+1. ✅ 使用同一个端口(通过 Nginx 代理)
+2. ✅ 使用同一个域名(localhost)
+3. ✅ 使用同一个协议(http)
+
+只要满足这三点,localStorage 就会自动共享!

+ 41 - 0
nginx.local.conf

@@ -0,0 +1,41 @@
+# Nginx 本地开发配置
+# 用于测试 React 项目和 JeecgBoot 的 localStorage 共享
+
+server {
+    listen 8080;
+    server_name localhost;
+
+    # React 项目(运行在 3000 端口)
+    location /spaces/ {
+        proxy_pass http://localhost:3000/spaces/;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        
+        # WebSocket 支持
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+    }
+
+    # JeecgBoot 前端(运行在 3100 端口)
+    location / {
+        proxy_pass http://localhost:3100/;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        
+        # WebSocket 支持
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+    }
+
+    # 后端 API(如果需要)
+    location /sohoyw-som/ {
+        proxy_pass http://52.83.132.95:8080/sohoyw-som/;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+}

+ 1 - 1
src/components/common/PermissionButton.tsx

@@ -1,5 +1,5 @@
 import { Button } from "@contentful/f36-components";
-import { useAuth } from "../../contexts/AuthContext";
+import { useAuth } from "@/contexts/AuthContext";
 import type { ComponentProps } from "react";
 
 interface PermissionButtonProps extends Omit<ComponentProps<typeof Button>, "children"> {

+ 1 - 1
src/components/common/PermissionWrapper.tsx

@@ -1,4 +1,4 @@
-import { useAuth } from "../../contexts/AuthContext";
+import { useAuth } from "@/contexts/AuthContext";
 
 import type { ReactNode } from "react";
 

+ 2 - 2
src/contexts/AuthContext.tsx

@@ -1,7 +1,7 @@
 import { createContext, useContext, useState, useEffect } from "react";
 import type { ReactNode } from "react";
-import type { JeecgMenu } from "../types/menu";
-import { getUserPermissions } from "../services/api";
+import type { JeecgMenu } from "@/types/menu";
+import { getUserPermissions } from "@/services/api";
 
 interface AuthContextType {
   menus: JeecgMenu[];

+ 1 - 1
src/contexts/MenuContext.tsx

@@ -4,7 +4,7 @@
  */
 
 import { createContext, useContext, useState, type ReactNode } from "react";
-import type { JeecgMenu } from "../types/menu";
+import type { JeecgMenu } from "@/types/menu";
 
 interface MenuContextType {
   activeFirstMenu: JeecgMenu | null;

+ 1 - 1
src/hooks/usePermission.ts

@@ -1,4 +1,4 @@
-import { useAuth } from "../contexts/AuthContext";
+import { useAuth } from "@/contexts/AuthContext";
 
 /**
  * 权限检查 Hook

+ 1 - 1
src/http/Axios.ts

@@ -5,7 +5,7 @@
 
 import type { AxiosRequestConfig, AxiosInstance, AxiosResponse, AxiosError } from "axios";
 import axios from "axios";
-import type { RequestOptions, Result } from "../types/axios";
+import type { RequestOptions, Result } from "@/types/axios";
 import type { CreateAxiosOptions } from "./axiosTransform";
 import { AxiosTransform } from "./axiosTransform";
 

+ 1 - 1
src/http/axiosTransform.ts

@@ -3,7 +3,7 @@
  */
 
 import type { AxiosRequestConfig, AxiosResponse } from "axios";
-import type { RequestOptions, Result } from "../types/axios";
+import type { RequestOptions, Result } from "@/types/axios";
 
 export interface CreateAxiosOptions extends AxiosRequestConfig {
   authenticationScheme?: string;

+ 2 - 2
src/http/checkStatus.ts

@@ -2,8 +2,8 @@
  * HTTP 状态码检查
  */
 
-import type { ErrorMessageMode } from "../types/axios";
-import message from "../utils/message";
+import type { ErrorMessageMode } from "@/types/axios";
+import message from "@/utils/message";
 
 export function checkStatus(status: number, msg: string, errorMessageMode: ErrorMessageMode = "message"): void {
   let errMessage = "";

+ 4 - 4
src/http/index.ts

@@ -4,14 +4,14 @@
  */
 
 import type { AxiosResponse } from "axios";
-import type { RequestOptions, Result } from "../types/axios";
+import type { RequestOptions, Result } from "@/types/axios";
 import type { AxiosTransform, CreateAxiosOptions } from "./axiosTransform";
 import { VAxios } from "./Axios";
 import { checkStatus } from "./checkStatus";
-import { API_BASE_URL, API_PREFIX, TIMEOUT } from "../config/api";
-import message from "../utils/message";
+import { API_BASE_URL, API_PREFIX, TIMEOUT } from "@/config/api";
+import message from "@/utils/message";
 import { joinTimestamp, formatRequestDate, setObjToUrlParams } from "./helper";
-import { getToken, clearAuth, getTenantId } from "../utils/auth";
+import { getToken, clearAuth, getTenantId } from "@/utils/auth";
 
 /**
  * 数据处理

+ 18 - 9
src/layouts/Header/index.tsx

@@ -4,12 +4,15 @@ import {
   QuestionIcon,
   GearSixIcon,
 } from "@contentful/f36-icons";
-import { useAuth } from "../../contexts/AuthContext";
-import { useMenu } from "../../contexts/MenuContext";
+import { useAuth } from "@/contexts/AuthContext";
+import { useMenu } from "@/contexts/MenuContext";
 import { useNavigate, useLocation } from "react-router-dom";
 import { useEffect, useState } from "react";
-import type { JeecgMenu } from "../../types/menu";
-import SearchModal from "../../components/SearchModal";
+import SearchModal from "@/components/SearchModal";
+import { navigateToJeecgByKey } from "@/utils/jeecgNavigation";
+
+import type { JeecgMenu } from "@/types/menu";
+
 
 export default function Header() {
   const { menus, loading } = useAuth();
@@ -19,8 +22,13 @@ export default function Header() {
   const location = useLocation();
   const [isSearchOpen, setIsSearchOpen] = useState(false);
 
+  const blackList = ["dashboard-analysis", "isystem"];
+
   // 获取顶级菜单(一级菜单)
-  const topMenus = menus.filter((menu) => !menu.hidden && !menu.meta?.hideMenu);
+  const topMenus = menus.filter(
+    (menu) =>
+      !menu.hidden && !menu.meta?.hideMenu && !blackList.includes(menu.name)
+  );
 
   // 根据当前路由自动设置活动菜单
   useEffect(() => {
@@ -68,8 +76,8 @@ export default function Header() {
 
   // 退出登录
   const loginOut = () => {
-    navigate('/login')
-  }
+    navigate("/login");
+  };
 
   if (loading) {
     return <div style={{ padding: "16px" }}>加载中...</div>;
@@ -185,8 +193,9 @@ export default function Header() {
       </Navbar.Item>
       <Navbar.Item label="设置菜单" icon={<GearSixIcon />}>
         <Navbar.MenuSectionTitle>通用</Navbar.MenuSectionTitle>
-        <Navbar.MenuItem title="首页" />
-        <Navbar.MenuItem title="API 密钥" />
+        <Navbar.MenuItem title="菜单设置" onClick={() => navigateToJeecgByKey('menu')}/>
+        <Navbar.MenuItem title="用户管理" />
+        <Navbar.MenuItem title="角色管理" />
         <Navbar.MenuSectionTitle>空间</Navbar.MenuSectionTitle>
         <Navbar.MenuItem title="应用" />
         <Navbar.MenuItem title="权限" onClick={toPermissionPage} />

+ 3 - 3
src/layouts/MainLayout/index.tsx

@@ -1,7 +1,7 @@
 import { Outlet } from "react-router-dom";
-import Header from "../Header/index";
-import Sidebar from "../Sidebar/index";
-import { useMenu } from "../../contexts/MenuContext";
+import Header from "@/layouts/Header/index";
+import Sidebar from "@/layouts/Sidebar/index";
+import { useMenu } from "@/contexts/MenuContext";
 import styles from "./index.module.css";
 
 export default function MainLayout() {

+ 2 - 2
src/layouts/Sidebar/index.tsx

@@ -1,7 +1,7 @@
 import { Text } from "@contentful/f36-components";
-import { useMenu } from "../../contexts/MenuContext";
+import { useMenu } from "@/contexts/MenuContext";
 import { useNavigate, useLocation } from "react-router-dom";
-import type { JeecgMenu } from "../../types/menu";
+import type { JeecgMenu } from "@/types/menu";
 import styles from "./index.module.css";
 
 interface SidebarItemProps {

+ 1 - 1
src/pages/content-model/index.tsx

@@ -14,7 +14,7 @@ import { QuestionIcon } from "@contentful/f36-icons";
 import {
   contentModelApi,
   type ContentModel,
-} from "../../services/modules/contentModel";
+} from "@/services/modules/contentModel";
 import styles from "./index.module.css";
 
 export default function ContentModelPage() {

+ 2 - 2
src/pages/login/hooks/useAuth.ts

@@ -2,8 +2,8 @@
  * 认证相关的React Hook
  */
 import { useState } from 'react';
-import { getToken, getUserInfo, clearAuth, setToken, setUserInfo, setLoginInfo } from '../../../utils/auth';
-import { authApi } from '../../../services/modules/system';
+import { getToken, getUserInfo, clearAuth, setToken, setUserInfo, setLoginInfo } from '@/utils/auth';
+import { authApi } from '@/services/modules/system';
 import { useNavigate } from 'react-router-dom';
 
 export interface LoginParams {

+ 2 - 2
src/pages/login/index.tsx

@@ -8,8 +8,8 @@ import {
   Note,
 } from "@contentful/f36-components";
 import { useNavigate } from "react-router-dom";
-import { authApi } from "../../services/modules/system";
-import { useAuth } from "../../contexts/AuthContext";
+import { authApi } from "@/services/modules/system";
+import { useAuth } from "@/contexts/AuthContext";
 import { useLoginAuth } from "./hooks/useAuth";
 import styles from "./index.module.css";
 

+ 3 - 3
src/router/index.tsx

@@ -4,10 +4,10 @@
 
 import { lazy, Suspense } from "react";
 import { createBrowserRouter, Navigate, type RouteObject } from "react-router-dom";
-import MainLayout from "../layouts/MainLayout/index";
-import LoginPage from "../pages/login/index";
+import MainLayout from "@/layouts/MainLayout/index";
+import LoginPage from "@/pages/login/index";
 
-import type { JeecgMenu } from "../types/menu";
+import type { JeecgMenu } from "@/types/menu";
 
 // 默认加载组件
 function LoadingPage() {

+ 1 - 1
src/services/BaseService.ts

@@ -3,7 +3,7 @@
  * 提供通用的 CRUD 操作,参考 JeecgBoot 的 CommonAPI
  */
 
-import { defHttp as http } from "../http";
+import { defHttp as http } from "@/http";
 
 export interface PageParams {
   pageNo: number;

+ 1 - 1
src/services/api.ts

@@ -3,7 +3,7 @@
  * 从各个模块导入并重新导出
  */
 
-import { defHttp } from "../http";
+import { defHttp } from "@/http";
 
 // 导出系统管理相关 API
 export { authApi, permissionApi, dictApi, uploadApi, sysApi } from "./modules/system";

+ 1 - 1
src/services/modules/contentModel.ts

@@ -2,7 +2,7 @@
  * 内容模型服务
  */
 
-import { defHttp } from "../../http";
+import { defHttp } from "@/http";
 
 export interface ContentModel {
   id: string;

+ 2 - 2
src/services/modules/system.ts

@@ -2,8 +2,8 @@
  * 系统管理服务
  */
 
-import { defHttp } from "../../http";
-import type { JeecgUserPermissions } from "../../types/menu";
+import { defHttp } from "@/http";
+import type { JeecgUserPermissions } from "@/types/menu";
 
 const http = defHttp;
 

+ 2 - 2
src/services/modules/user.ts

@@ -2,8 +2,8 @@
  * 用户管理服务
  */
 
-import { BaseService } from "../BaseService";
-import { defHttp as http } from "../../http";
+import { BaseService } from "@/services/BaseService";
+import { defHttp as http } from "@/http";
 
 export interface User {
   id: string;

+ 1 - 1
src/utils/auth.ts

@@ -4,7 +4,7 @@
  * 使用内存缓存优先机制,提高性能
  */
 
-import { generatePersistentKey } from '../config/project';
+import { generatePersistentKey } from '@/config/project';
 
 // JeecgBoot Persistent 缓存的主 key
 // 格式:{项目名}__{环境}__{版本}__COMMON__LOCAL__KEY__

+ 132 - 0
src/utils/jeecgNavigation.ts

@@ -0,0 +1,132 @@
+/**
+ * JeecgBoot 页面跳转工具
+ * 利用共享的 localStorage 实现免登录跳转
+ */
+
+/**
+ * JeecgBoot 页面配置
+ */
+export interface JeecgPageConfig {
+  /** 页面标识 */
+  key: string;
+  /** 页面标题 */
+  title: string;
+  /** JeecgBoot 路径 */
+  path: string;
+  /** 描述 */
+  description?: string;
+}
+
+/**
+ * 系统设置页面
+ */
+export const systemPages: JeecgPageConfig[] = [
+  {
+    key: 'user',
+    title: '用户管理',
+    path: '/system/user',
+    description: '管理系统用户',
+  },
+  {
+    key: 'role',
+    title: '角色管理',
+    path: '/system/role',
+    description: '管理用户角色和权限',
+  },
+  {
+    key: 'menu',
+    title: '菜单管理',
+    path: '/system/menu',
+    description: '管理系统菜单和权限',
+  },
+  {
+    key: 'depart',
+    title: '部门管理',
+    path: '/system/depart',
+    description: '管理组织架构',
+  },
+  {
+    key: 'dict',
+    title: '字典管理',
+    path: '/system/dict',
+    description: '管理数据字典',
+  },
+  {
+    key: 'log',
+    title: '日志管理',
+    path: '/system/log',
+    description: '查看系统日志',
+  },
+];
+
+/**
+ * 跳转到 JeecgBoot 页面
+ * @param path JeecgBoot 页面路径
+ * @param newTab 是否在新标签页打开
+ */
+export function navigateToJeecg(path: string, newTab: boolean = false): void {
+  // 确保路径以 / 开头
+  const normalizedPath = path.startsWith('/') ? path : `/${path}`;
+  
+  // 生产环境:同域名,直接跳转
+  // 开发环境:不同端口,localStorage 不共享,需要通过 URL 参数传递 token
+  if (import.meta.env.PROD) {
+    // 生产环境:直接跳转
+    const url = normalizedPath;
+    if (newTab) {
+      window.open(url, '_blank');
+    } else {
+      window.location.href = url;
+    }
+  } else {
+    // 开发环境:通过 URL 参数传递认证信息
+    const token = localStorage.getItem('内容中心管理系统__PRODUCTION__1.0.0__COMMON__LOCAL__KEY__');
+    const url = `http://localhost:3100${normalizedPath}`;
+    
+    if (token) {
+      // 将 token 通过 URL 参数传递(仅用于开发环境测试)
+      const urlWithToken = `${url}?_dev_token=${encodeURIComponent(token)}`;
+      
+      if (newTab) {
+        window.open(urlWithToken, '_blank');
+      } else {
+        window.location.href = urlWithToken;
+      }
+      
+      console.warn('[Dev Only] Token passed via URL parameter for local testing');
+    } else {
+      // 没有 token,直接跳转
+      if (newTab) {
+        window.open(url, '_blank');
+      } else {
+        window.location.href = url;
+      }
+    }
+  }
+}
+
+/**
+ * 根据 key 跳转到 JeecgBoot 页面
+ * @param key 页面标识
+ * @param newTab 是否在新标签页打开
+ */
+export function navigateToJeecgByKey(key: string, newTab: boolean = false): void {
+  const page = systemPages.find(p => p.key === key);
+  if (page) {
+    navigateToJeecg(page.path, newTab);
+  } else {
+    console.error(`[JeecgNavigation] Page not found: ${key}`);
+  }
+}
+
+/**
+ * 获取 JeecgBoot 页面完整 URL
+ * @param path JeecgBoot 页面路径
+ */
+export function getJeecgUrl(path: string): string {
+  const normalizedPath = path.startsWith('/') ? path : `/${path}`;
+  
+  return import.meta.env.PROD 
+    ? `${window.location.origin}${normalizedPath}`
+    : `http://localhost:3100${normalizedPath}`;
+}

+ 7 - 1
tsconfig.app.json

@@ -22,7 +22,13 @@
     "noUnusedParameters": true,
     "erasableSyntaxOnly": true,
     "noFallthroughCasesInSwitch": true,
-    "noUncheckedSideEffectImports": true
+    "noUncheckedSideEffectImports": true,
+
+    /* Path Mapping */
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"]
+    }
   },
   "include": ["src"]
 }