first commit

This commit is contained in:
2026-06-03 10:09:03 +08:00
commit 81f5039458
6266 changed files with 188236 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
/**
* 通用的别名路径解析工具
* 用于在动态导入时解析 @ 别名路径
*/
import { h } from 'vue';
// 使用 import.meta.glob 预加载所有组件
const viewsModules = import.meta.glob('../views/**/*.vue');
// 创建路径映射表
const pathMap = new Map();
// 初始化路径映射
Object.keys(viewsModules).forEach(relativePath => {
// relativePath 示例: ../views/system/users.vue
// 统一去掉扩展名进行存储,方便各种格式匹配
const baseNoExt = relativePath.replace('../views/', '').replace('.vue', '');
const baseWithExt = relativePath.replace('../views/', '');
// 1. 存储标准路径
pathMap.set(relativePath, viewsModules[relativePath]);
// 2. 存储 @/views 路径
pathMap.set(relativePath.replace('../views', '@/views'), viewsModules[relativePath]);
// 3. 存储 /system/users 格式(不带扩展名)
pathMap.set(`/${baseNoExt}`, viewsModules[relativePath]);
// 4. 存储 system/users 格式(不带扩展名)
pathMap.set(baseNoExt, viewsModules[relativePath]);
// 5. 存储 /system/users.vue 格式(带扩展名)
pathMap.set(`/${baseWithExt}`, viewsModules[relativePath]);
// 6. 存储 system/users.vue 格式(带扩展名)
pathMap.set(baseWithExt, viewsModules[relativePath]);
});
/**
* 解析别名路径为实际模块加载器
* @param {string} path - 支持的路径格式:
* - @/views/dashboard/index.vue (别名格式)
* - /dashboard/index.vue (数据库格式,带前导斜杠)
* - dashboard/index.vue (相对格式)
* @returns {Function|null} 返回模块加载器函数,找不到时返回 null
*/
export function resolveComponent(path) {
if (!path) return null;
// 预处理 path:去掉可能的 .vue 后缀统一查找
const cleanPath = path.replace('.vue', '');
// 尝试直接匹配
const loader = pathMap.get(path) || pathMap.get(cleanPath);
if (loader) return loader;
// 数据库格式补全匹配 (针对 /system/users)
const dbFormat = cleanPath.startsWith('/') ? cleanPath : `/${cleanPath}`;
if (pathMap.get(dbFormat)) return pathMap.get(dbFormat);
// 模糊匹配:文件名匹配
const fileName = cleanPath.split('/').pop();
for (const [mappedPath, loader] of pathMap.entries()) {
if (mappedPath.endsWith(`${fileName}.vue`) || mappedPath.endsWith(fileName)) {
return loader;
}
}
return null;
}
/**
* 创建组件加载器
* @param {string} componentPath - 组件路径
* @returns {Function} Vue 路由组件加载函数
*/
export function createComponentLoader(componentPath) {
const loader = resolveComponent(componentPath);
if (loader) return loader;
console.error(`❌ [路由错误] 未找到组件: ${componentPath}`);
// 返回一个标准的 Vue 组件对象,确保 Router 不报错
return () => Promise.resolve({
name: 'ComponentNotFound',
render: () => {
import('element-plus').then(El => El.ElMessage.error(`路径错误: ${componentPath}`));
return h('div', { style: 'padding:20px; color:red;' }, `组件路径不存在: ${componentPath}`);
}
});
}
/**
* 获取所有已加载的模块路径(用于调试)
* @returns {Array<string>} 所有可用的路径列表
*/
export function getAllModulePaths() {
return Array.from(pathMap.keys());
}
+252
View File
@@ -0,0 +1,252 @@
import request from '@/utils/request';
import * as qiniu from 'qiniu-js';
/**
* 获取存储配置
* @returns {Promise<{storageType: string, qiniuDomain?: string, qiniuRegion?: string}>}
*/
export async function getStorageConfig() {
const res = await request({
url: '/platform/storage/config',
method: 'get',
});
if (res?.code === 200) {
return res.data || { storageType: 'local' };
}
return { storageType: 'local' };
}
/**
* 获取七牛云上传凭证
* @returns {Promise}
*/
export async function getQiniuToken() {
return request({
url: '/platform/qiniu/token',
method: 'get',
});
}
/**
* 保存文件记录到数据库
* @param {Object} data 文件信息
* @returns {Promise}
*/
export async function saveFileRecord(data) {
return request({
url: '/platform/qiniu/save',
method: 'post',
data,
});
}
/**
* 上传文件(自动选择本地或七牛云)
* @param {File} file 文件对象
* @param {Object} options 配置选项
* @param {number} [options.cate] 文件分类
* @param {Function} [options.onProgress] 进度回调
* @returns {Promise<{url: string, id: number, name: string, key?: string}>}
*/
export async function smartUpload(file, options = {}) {
// 获取存储配置
const config = await getStorageConfig();
if (config.storageType === 'qiniu') {
// 使用七牛云直传
return uploadToQiniu(file, options);
} else {
// 使用本地上传(通过后端)
return uploadToLocal(file, options);
}
}
/**
* 上传到七牛云(直传)
* @param {File} file 文件对象
* @param {Object} options 配置选项
* @returns {Promise}
*/
export async function uploadToQiniu(file, options = {}) {
// 1. 获取上传凭证
const tokenRes = await getQiniuToken();
if (tokenRes?.code !== 200) {
throw new Error(tokenRes?.msg || '获取上传凭证失败');
}
const { token, keyPrefix, domain, region, uploadUrl } = tokenRes.data;
// 2. 生成文件 key
const ext = file.name.split('.').pop();
const key = `${keyPrefix}.${ext}`;
// 3. 配置上传参数
const putExtra = {
fname: file.name,
mimeType: file.type || 'application/octet-stream',
};
// 4. 根据区域代码获取七牛云区域对象
const qiniuRegion = getQiniuRegion(region);
const config = {
useCdnDomain: true,
region: qiniuRegion,
};
// 5. 创建 observable 对象
const observable = qiniu.upload(file, key, token, putExtra, config);
// 5. 执行上传
return new Promise((resolve, reject) => {
const subscription = observable.subscribe({
next(res) {
// 进度回调
if (options.onProgress) {
options.onProgress({
loaded: res.total.loaded,
total: res.total.size,
percent: res.total.percent,
});
}
},
error(err) {
reject(new Error(err.message || '上传失败'));
},
async complete(res) {
try {
// 6. 保存文件记录到数据库
const saveRes = await saveFileRecord({
key: res.key,
hash: res.hash,
size: file.size,
name: file.name,
mimeType: file.type,
cate: options.cate || 0,
});
if (saveRes?.code === 200 || saveRes?.code === 201) {
resolve({
url: saveRes.data.url,
id: saveRes.data.id,
name: saveRes.data.name,
key: saveRes.data.key,
});
} else {
reject(new Error(saveRes?.msg || '保存文件记录失败'));
}
} catch (error) {
reject(error);
}
},
});
});
}
/**
* 上传到本地(通过后端中转)
* @param {File} file 文件对象
* @param {Object} options 配置选项
* @returns {Promise}
*/
export async function uploadToLocal(file, options = {}) {
const formData = new FormData();
formData.append('file', file);
if (options.cate !== undefined) {
formData.append('cate', String(options.cate));
}
const config = {
url: '/platform/uploadfile',
method: 'post',
data: formData,
timeout: 0, // 不设置超时
};
if (options.onProgress) {
config.onUploadProgress = (e) => {
options.onProgress({
loaded: e.loaded,
total: e.total || 0,
percent: e.total > 0 ? Math.round((e.loaded * 100) / e.total) : 0,
});
};
}
const res = await request(config);
if (res?.code === 200 || res?.code === 201) {
return {
url: res.data.url,
id: res.data.id,
name: res.data.name,
};
} else {
throw new Error(res?.msg || '上传失败');
}
}
/**
* 批量上传文件
* @param {File[]} files 文件数组
* @param {Object} options 配置选项
* @param {Function} [options.onFileProgress] 单个文件进度回调 (file, progress) => void
* @param {Function} [options.onFileComplete] 单个文件完成回调 (file, result) => void
* @param {Function} [options.onFileError] 单个文件错误回调 (file, error) => void
* @returns {Promise<Array>}
*/
export async function batchUpload(files, options = {}) {
const results = [];
for (const file of files) {
try {
const result = await smartUpload(file, {
...options,
onProgress: (progress) => {
if (options.onFileProgress) {
options.onFileProgress(file, progress);
}
},
});
results.push({ file, result, success: true });
if (options.onFileComplete) {
options.onFileComplete(file, result);
}
} catch (error) {
results.push({ file, error, success: false });
if (options.onFileError) {
options.onFileError(file, error);
}
}
}
return results;
}
/**
* 根据区域代码获取七牛云区域对象
* @param {string} regionCode 区域代码 (z0, z1, z2, na0, as0, cn-east-2)
* @returns {Object} 七牛云区域对象
*/
function getQiniuRegion(regionCode) {
switch (regionCode) {
case 'z0':
return qiniu.region.z0; // 华东
case 'z1':
return qiniu.region.z1; // 华北
case 'z2':
return qiniu.region.z2; // 华南
case 'na0':
return qiniu.region.na0; // 北美
case 'as0':
return qiniu.region.as0; // 新加坡
case 'cn-east-2':
return qiniu.region.cnEast2; // 华东-浙江2
default:
return qiniu.region.z0; // 默认华东
}
}
+68
View File
@@ -0,0 +1,68 @@
import axios from 'axios';
// 获取API基础URL;开发环境可在 .env.development 留空,配合 Vite 代理访问 /platform
const apiBaseURL = import.meta.env.VITE_API_BASE_URL ?? "";
// 创建axios实例(普通接口 5min;大文件上传在 api/file.js 单独更长 timeout
const service = axios.create({
baseURL: apiBaseURL,
timeout: 300000,
withCredentials: false // JWT 不需要 Cookie
});
// 请求拦截器
service.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
// 对于有 body 的请求(POST、PUT、PATCH),默认 JSONFormData 由浏览器带 multipart boundary,不可手写 Content-Type
if (config.data && ['post', 'put', 'patch'].includes(config.method?.toLowerCase())) {
if (config.data instanceof FormData) {
delete config.headers['Content-Type'];
delete config.headers['content-type'];
} else if (!config.headers['Content-Type'] && !config.headers['content-type']) {
config.headers['Content-Type'] = 'application/json';
}
}
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
response => {
return response.data;
},
error => {
if (error.response) {
switch (error.response.status) {
case 401:
console.error('未授权,请重新登录');
localStorage.removeItem('token');
localStorage.removeItem('userInfo');
if (window.location.hash !== '#/login') {
window.location.href = '#/login';
}
return Promise.reject(new Error('token无效'));
case 404:
console.error('请求的资源不存在');
break;
default:
console.error('请求失败,请稍后再试');
}
} else if (error.request) {
console.error('请求失败,请检查网络连接');
} else {
console.error('请求配置错误');
}
return Promise.reject(error);
}
);
export default service;
+42
View File
@@ -0,0 +1,42 @@
/**
* URL工具函数
*/
/**
* 获取完整的文件URL
* 如果URL已经是完整URLhttp://或https://开头),直接返回
* 否则拼接API基础URL
* @param {string} url - 文件URL或路径
* @returns {string} 完整的URL
*/
export function getFileUrl(url) {
if (!url) return '';
// 如果URL已经是完整的URL(以http://或https://开头),直接返回
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// 否则拼接API基础URL
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
return `${API_BASE_URL}${url}`;
}
/**
* 获取环境URLgetEnvUrl的别名)
* @param {string} path - 文件路径
* @returns {string} 完整的URL
*/
export function getEnvUrl(path) {
return getFileUrl(path);
}
/**
* 判断URL是否是完整URL
* @param {string} url - URL字符串
* @returns {boolean} 是否是完整URL
*/
export function isFullUrl(url) {
if (!url) return false;
return url.startsWith('http://') || url.startsWith('https://');
}