diff --git a/README.md b/README.md index 452a007d..8256b898 100644 --- a/README.md +++ b/README.md @@ -4,124 +4,71 @@ **`master-fetch` 分支是带有后端接口请求的分支** -**后端项目地址:[https://gitee.com/MTrun/go-view-serve](https://gitee.com/MTrun/go-view-serve)** -**接口说明地址:[https://docs.apipost.cn/preview/5aa85d10a59d66ce/ddb813732007ad2b?target_id=84dbc5b0-158f-4bcb-8f74-793ac604ada3#3e053622-1e76-43f9-a039-756aee822dbb](https://docs.apipost.cn/preview/5aa85d10a59d66ce/ddb813732007ad2b?target_id=84dbc5b0-158f-4bcb-8f74-793ac604ada3#3e053622-1e76-43f9-a039-756aee822dbb)** +### feat-unify-test 分支目标 ++ 实现 backend 后端工厂 +将后端业务逻辑集中到 backend 了,控制 BackEndFactory 就可以适配不同的后端。 +伪代码如下: +export const BackEndFactory = ():IBackend=>{ +switch(项目后端配置){ +case "无数据库": +return new MockBackend() // 等同: -master ,没有存储 +case "indexdb": +return new IndexDbBackend() // 这次开发的,用 indexdb 做测试 +case "java": +return new JavaBackend() // 等同: -fetch, 没 java 环境,还没做 +case "python": +return new PythonBackend() // 自定义开发的后端 +。。。 其他 oss 、云平台的后端 。。。 +}} +意义: +1 unify 统一 -fetch 和 master 分支,消除分支之间的差异。 +2 方便接入不同的自定义后端平台。 +3 前端存储功能让测试工作更加方便 -## 使用 ++ 完善事件处理机制 +在事件中修改图表配置 +在事件中修改图表数据 +在事件中调用图表 exposed 函数 +数据驱动界面 -所有的接口地址位置:`src\api\path\*` +### 试验功能1:Backend 后端工厂 ++ 对比 -fetch 分支,梳理后端逻辑到 backend 目录的 ibackend 接口 + + 登录 - login + + 登出 - logout + + 预览,token 注入或单点登陆 - checkToken + + 显示项目列表和分页 - projectList + + 保存、发布、修改名称 - updateProject + + 复制项目 - copyProject + + 图表内的图片上传 - uploadFile + + 上传图片显示处理 - getFileUrl ++ IndexDbBackend 用indexdb浏览器数据库实现了 project 相关所有功能。 ++ Todo: 统一后端错误处理 ++ Todo:开发 javabackend,适配现有的后端 -接口地址修改:`.env` +### 试验功能2:事件处理机制 ++ 实现最常用的互动:找到图表元素、显示或隐藏、修改数据 ++ 核心代码:useLifeHandler.hook.ts ++ 在事件代码中通过 runtime 实现运行时刻的图表管理,提供基础函数: + + selectComponents 选择多个图表 + + selectOneComponent 选择一个图表 + + getChartConfig 读取图表 + + setChartConfig 设置图表 + + callExposed 调用图表 exposed 的函数 ++ 以下例子可以在点击事件中加入代码并预览,测试效果。 -### 🤯 后端项目 ++ 例子1 切换显示名称为 饼图 和 柱状图 的图表: +const range = runtime.fn.selectComponents("饼图 柱状图") +const h = runtime.fn.getChartConfig(range, "hide") +runtime.fn.setChartConfig(range, "hide", !h) -后端项目gitee地址:[https://gitee.com/MTrun/go-view-serve](https://gitee.com/MTrun/go-view-serve) ++ 例子2 修改一个名称 柱状图001 组件id 2wolqibrx3c000 的图表数据,以下两句等效 +runtime.fn.setChartConfig("柱状图001", "dataset", {"dimensions":["product","data1","data2"],"source":[{"product":"Mon","data1":120,"data2":130}]}) +runtime.fn.setChartConfig("#2wolqibrx3c000", "dataset", {"dimensions":["product","data1","data2"],"source":[{"product":"Mon","data1":120,"data2":230}]}) -接口说明地址:[https://docs.apipost.cn/preview/5aa85d10a59d66ce/ddb813732007ad2b?target_id=84dbc5b0-158f-4bcb-8f74-793ac604ada3#3e053622-1e76-43f9-a039-756aee822dbb](https://docs.apipost.cn/preview/5aa85d10a59d66ce/ddb813732007ad2b?target_id=84dbc5b0-158f-4bcb-8f74-793ac604ada3#3e053622-1e76-43f9-a039-756aee822dbb) - -```shell -# port -VITE_DEV_PORT = '8080' - -# development path -VITE_DEV_PATH = 'http://127.0.0.1:8080' - -# production path -VITE_PRO_PATH = 'http://127.0.0.1:8080' -``` - -公共前缀修改:`src\settings\httpSetting.ts` - -```shell -// 请求前缀 -export const axiosPre = '/api/goview' -``` - -接口封装:`src\api\http.ts` - -```ts -import axiosInstance from './axios' -import { RequestHttpEnum, ContentTypeEnum } from '@/enums/httpEnum' - -export const get = (url: string, params?: object) => { - return axiosInstance({ - url: url, - method: RequestHttpEnum.GET, - params: params, - }) -} - -export const post = (url: string, data?: object, headersType?: string) => { - return axiosInstance({ - url: url, - method: RequestHttpEnum.POST, - data: data, - headers: { - 'Content-Type': headersType || ContentTypeEnum.JSON - } - }) -} - -export const put = (url: string, data?: object, headersType?: string) => { - return axiosInstance({ - url: url, - method: RequestHttpEnum.PUT, - data: data, - headers: { - 'Content-Type': headersType || ContentTypeEnum.JSON - } - }) -} - -export const del = (url: string, params?: object) => { - return axiosInstance({ - url: url, - method: RequestHttpEnum.DELETE, - params - }) -} - -// 获取请求函数,默认get -export const http = (type?: RequestHttpEnum) => { - switch (type) { - case RequestHttpEnum.GET: - return get - - case RequestHttpEnum.POST: - return post - - case RequestHttpEnum.PUT: - return put - - case RequestHttpEnum.DELETE: - return del - - default: - return get - } -} - -``` - -## 代码提交 - -* feat: 新功能 -* fix: 修复 Bug -* docs: 文档修改 -* perf: 性能优化 -* revert: 版本回退 -* ci: CICD集成相关 -* test: 添加测试代码 -* refactor: 代码重构 -* build: 影响项目构建或依赖修改 -* style: 不影响程序逻辑的代码修改 -* chore: 不属于以上类型的其他类型(日常事务) - -## 交流 - -QQ 群:1030129384 - -![QQ群](readme/go-view-qq.png) - -![渲染海报](readme/logo-poster.png) ++ 例子3 找到一个组并隐藏 +const c = runtime.fn.selectOneComponent("分组") +if(c){ + console.log(runtime.fn.getChartConfig(c, "isGroup" )) + runtime.fn.setChartConfig(c, "hide", true) +} \ No newline at end of file diff --git a/src/api/axios.ts b/src/api/axios.ts index bb5b0269..e47f67be 100644 --- a/src/api/axios.ts +++ b/src/api/axios.ts @@ -1,38 +1,19 @@ import axios, { AxiosResponse, AxiosRequestConfig } from 'axios' import { ResultEnum } from "@/enums/httpEnum" -import { PageEnum, ErrorPageNameMap } from "@/enums/pageEnum" -import { StorageEnum } from '@/enums/storageEnum' -import { axiosPre } from '@/settings/httpSetting' -import { SystemStoreEnum, SystemStoreUserInfoEnum } from '@/store/modules/systemStore/systemStore.d' -import { redirectErrorPage, getLocalStorage, routerTurnByName, httpErrorHandle } from '@/utils' -import { fetchAllowList } from './axios.config' -import includes from 'lodash/includes' +import { ErrorPageNameMap } from "@/enums/pageEnum" +import { redirectErrorPage } from '@/utils' const axiosInstance = axios.create({ - baseURL: `${import.meta.env.PROD ? import.meta.env.VITE_PRO_PATH : ''}${axiosPre}`, + baseURL: import.meta.env.DEV ? import.meta.env.VITE_DEV_PATH : import.meta.env.VITE_PRO_PATH, timeout: ResultEnum.TIMEOUT, }) axiosInstance.interceptors.request.use( (config: AxiosRequestConfig) => { - // 白名单校验 - if (includes(fetchAllowList, config.url)) return config - // 获取 token - const info = getLocalStorage(StorageEnum.GO_SYSTEM_STORE) - // 重新登录 - if (!info) { - routerTurnByName(PageEnum.BASE_LOGIN_NAME) - return config - } - const userInfo = info[SystemStoreEnum.USER_INFO] - config.headers = { - ...config.headers, - [userInfo[SystemStoreUserInfoEnum.TOKEN_NAME] || 'token']: userInfo[SystemStoreUserInfoEnum.USER_TOKEN] || '' - } return config }, - (err: AxiosRequestConfig) => { - Promise.reject(err) + (error: AxiosRequestConfig) => { + Promise.reject(error) } ) @@ -40,27 +21,9 @@ axiosInstance.interceptors.request.use( axiosInstance.interceptors.response.use( (res: AxiosResponse) => { const { code } = res.data as { code: number } - - // 成功 - if (code === ResultEnum.SUCCESS) { - return Promise.resolve(res.data) - } - - // 登录过期 - if (code === ResultEnum.TOKEN_OVERDUE) { - window['$message'].error(window['$t']('http.token_overdue_message')) - routerTurnByName(PageEnum.BASE_LOGIN_NAME) - return Promise.resolve(res.data) - } - - // 固定错误码重定向 - if (ErrorPageNameMap.get(code)) { - redirectErrorPage(code) - return Promise.resolve(res.data) - } - - // 提示错误 - window['$message'].error(window['$t']((res.data as any).msg)) + if (code === ResultEnum.SUCCESS) return Promise.resolve(res.data) + // 重定向 + if (ErrorPageNameMap.get(code)) redirectErrorPage(code) return Promise.resolve(res.data) }, (err: AxiosResponse) => { diff --git a/src/api/http.ts b/src/api/http.ts index 534f0c0e..16307d15 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -13,7 +13,7 @@ export const get = (url: string, params?: object) => { return axiosInstance({ url: url, method: RequestHttpEnum.GET, - params: params, + params: params }) } diff --git a/src/assets/images/chart/informations/text_barrage.png b/src/assets/images/chart/informations/text_barrage.png new file mode 100644 index 00000000..3b7db8ef Binary files /dev/null and b/src/assets/images/chart/informations/text_barrage.png differ diff --git a/src/assets/images/project/moke-20211219181327.png b/src/assets/images/project/moke-20211219181327.png new file mode 100644 index 00000000..7be19aa3 Binary files /dev/null and b/src/assets/images/project/moke-20211219181327.png differ diff --git a/src/backend/ibackend.ts b/src/backend/ibackend.ts new file mode 100644 index 00000000..7ece937c --- /dev/null +++ b/src/backend/ibackend.ts @@ -0,0 +1,146 @@ + +/** + * 后端接口,相关功能对应表: + * 登录 - login + * 登出 - logout + * 预览,token 注入或单点登陆 - checkToken + * 显示项目列表和分页 - projectList + * 保存、发布、修改名称 - updateProject + * 复制项目 - copyProject + * 图表内的图片上传 - uploadFile + * 上传图片显示处理 - getFileUrl + * 所有接口返回格式:MyResponseType + */ +import { IndexDbBackend } from "./indexdb/indexdbbackend"; +// import { PythonBackend } from "./python/pythonbackend"; + +export interface MyResponseType { + code: number; // 状态:200 表示接口调用成功,参考:HttpEnum + msg: string; // 提示信息,配合 data 和 code + data: any; // data = null 表示接口结果错误,错误原因放在 msg +} + +export class MyResponse implements MyResponseType { + code: number = 200; + msg: string = ""; + data: any = {}; +} + +/** + * 实现 IBackend 后端接口 + * 错误处理: + */ +export interface IBackend { + /** + * 初始化后端系统,测试后端连接,oss地址等 + * @param data 可选,备用 + */ + init(data:any):any + + /** + * 登陆 + * @param data {} .username .password + * @return MyResponseType + * .data 须包含: + * token:{tokenValue:"", tokenName:""}, + * userinfo:{nickname:"", username: "", id: 用户ID} + * 错误处理: + * 1 接口错误 .code 不为 200 .msg 可选,后端反馈错误信息 + * 2 登陆错误 .code=200 .data = null, msg 可选,反馈不能登陆的原因 + * 登陆信息.data 记录: + * setLocalStorage(GO_LOGIN_INFO_STORE, res.data) + */ + login(data:any):any + + /** + * 通知后端登出 + */ + logout():any + + /** + * 检查Token是否有效,配合预览页面和单点登陆,备用 + * @param data {tokenValue, tokenName} + * @return 同 login() + */ + checkToken(data:any):any + + /** + * 项目列表 + * @param data {} .page, .limit + * @return [项目],字段名称需要进行 map + * id: projectId + * title:projectName + * release, + * label:remarks + * image:indexImage 如果需要挂刷新,在这里处理。如果需要拼接 url(getFileUrl),也在这里处理好。 + */ + projectList(data:any):any + + /** + * 新增项目 + * @param data + * .projectName + * @return id 新项目 ID + */ + createProject(data: any):any + + /** + * 获取项目 + * @param data .projectId + * @return + id:projectId + projectName, + state: release, + remarks, + content + */ + fetchProject(data: any):any + + /** + * 修改项目 + * @param data + * .projectId 必须 + * .projectName 可选 + * .release 可选 + * .content 可选 + * .object File 可选 对象 + * .remarks 可选 + * @return + */ + updateProject(data: any):any + + /** + * 复制项目 + * @param data + * .copyId 需要复制的项目ID + * .projectName + * @return id 新项目ID + */ + copyProject(data: any):any + + /** + * 删除项目 + * @param data + * .projectId + * @return + */ + deleteProject(data: any):any + + /** + * 文件上传 + * @param file File 图片对象 + * @param params 备用 Todo: 上传文件可带上项目ID和其他附加信息,以便后端文件管理 + * @return .uri 文件对象 uri。建议在图表中保存相对地址,通过 getFileUrl 得到完整地址 + */ + uploadFile(file: File, params: any):any + /** + * 文件地址转换,处理 uploadFile 的返回地址。如果是绝对地址,可以不处理 + * @param uploadUri 上传返回的 uri + * @return 供 image.src 使用的地址信息 + */ + getFileUrl(uploadUri:string):string +} + +export const BackEndFactory = new IndexDbBackend(); +// export const BackEndFactory = new MockBackend(); +// export const BackEndFactory = new PythonBackend(); \ No newline at end of file diff --git a/src/backend/indexdb/indexdb.ts b/src/backend/indexdb/indexdb.ts new file mode 100644 index 00000000..2e17c49b --- /dev/null +++ b/src/backend/indexdb/indexdb.ts @@ -0,0 +1,147 @@ +/** + * IndexDb 帮助类 + */ + +const win: { [k: string]: any } = window || globalThis; +const indexedDB = + win.indexedDB || win.mozIndexedDB || win.webkitIndexedDB || win.msIndexedDB; +const dbs: { [k: string]: IDBDatabase } = {}; +let databaseName: string; +let request: IDBOpenDBRequest; +interface AnyEvent { + [k: string]: any; +} + +export interface TableOption { + storeName: string; + option: { [K: string]: any }; + index: { [K: string]: any }[]; +} + +export const createDB = ( + name: string, + version?: string, + options?: TableOption[], +) => + new Promise((resolve, reject) => { + if (!indexedDB) reject('浏览器不支持indexedDB'); + databaseName = name; + if (dbs?.[name]) { + resolve(dbs[name]); + return; + } + request = indexedDB.open(name, version); + createTable(options)?.then((db: IDBDatabase) => resolve(db)); + request.onsuccess = (event: AnyEvent) => { + // IDBDatabase + const db = event.target.result; + // 缓存起来 + dbs[name] = db; + resolve(db); + }; + request.onerror = (event: AnyEvent) => reject(event); + }); + +export const createTable = (options?: TableOption[]) => { + if (!options) return; + return new Promise((resolve) => { + request.onupgradeneeded = (event: AnyEvent) => { + const db = event.target.result; + dbs[databaseName] = db; + for (const i in options) { + // 判断是否存在表 + if (!db.objectStoreNames.contains(options[i].storeName)) { + const objectStore = db.createObjectStore( + options[i].storeName, + options[i].option, + ); + for (const j of options[i].index) { + objectStore.createIndex(j.name, j.keyPath, { + unique: j.unique, + }); + } + } + } + resolve(db); + }; + }); +}; + +const getTransaction = async (name: string, version?: string) => { + let db: IDBDatabase; + // 先从缓存获取 + if (dbs[databaseName]) { + db = dbs[databaseName]; + } else { + db = await createDB(databaseName, version); + } + return db.transaction(name, 'readwrite'); +}; + +const getObjectStore = async ( + name: string, + version?: string, +): Promise => { + const transaction = await getTransaction(name, version); + return transaction.objectStore(name); +}; + +const getStore = (name: string, type: string, data: any) => + new Promise((resolve) => { + getObjectStore(name).then((objectStore: IDBObjectStore | any) => { + const request = objectStore[type](data); + request.onsuccess = (event: AnyEvent) => + resolve(event.target.result); + }); + }); + + +const findStore = ( + name: string, + start: any, + end: any, + startInclude: any, + endInclude: any, +) => + new Promise((resolve, reject) => { + getObjectStore(name).then((objectStore: IDBObjectStore) => { + const request = objectStore.openCursor( + IDBKeyRange.bound(start, end, startInclude, endInclude), + ); + request.onsuccess = (event: AnyEvent) => + resolve(event.target.result); + request.onerror = (event: AnyEvent) => reject(event); + }); + }); + +export interface DBSelect { + add: (data: any) => Promise; + get: (data: any) => Promise; + getAll: () => Promise; + del: (data: any) => Promise; + clear: (data: any) => Promise; + put: (data: any) => Promise; + find: ( + start: any, + end: any, + startInclude: any, + endInclude: any, + ) => Promise; +} +// 获取一个store +export const onDBSelect = async ( + name: string, + version: string +): Promise => { + const add = (data: any) => getStore(name, 'add', data); + const get = (data: any) => getStore(name, 'get', data); + const getAll = () => getStore(name, 'getAll', null); + const del = (data: any) => getStore(name, 'delete', data); + const clear = (data: any) => getStore(name, 'clear', data); + const put = (data: any) => getStore(name, 'put', data); + const find = (start: any, end: any, startInclude: any, endInclude: any) => + findStore(name, start, end, startInclude, endInclude); + const options: DBSelect = { add, get, getAll, clear, del, put, find }; + getObjectStore(name, version); + return options; +}; \ No newline at end of file diff --git a/src/backend/indexdb/indexdbbackend.ts b/src/backend/indexdb/indexdbbackend.ts new file mode 100644 index 00000000..9eb23be6 --- /dev/null +++ b/src/backend/indexdb/indexdbbackend.ts @@ -0,0 +1,155 @@ +import { MyResponse, IBackend } from '../ibackend' +import { createDB, DBSelect, onDBSelect } from '../indexdb/indexdb' +import { fileToUrl, fileToBlob } from "@/utils" + +const PROJECT_TABLE = "project" +const IMAGE_TABLE = "image" // 保存图片,未实现,Todo +const DB_NAME = "goview" +const DB_VER = "1" + +export class IndexDbBackend implements IBackend { + public async init(data: any) { + let rtn:MyResponse = new MyResponse; + const db:IDBDatabase = await createDB(DB_NAME, DB_VER, [ + { + storeName: PROJECT_TABLE, + option: { + keyPath: "projectId", autoIncrement:true + }, + index: [ + {name: 'projectId', keyPath: "projectId", unique: true}, + {name: 'projectName', keyPath: "projectName", unique: false}, + {name: 'release', keyPath: "release", unique: false}, + {name: 'remarks', keyPath: "remarks", unique: false}, + {name: 'content', keyPath: "content", unique: false}, + {name: 'indexImage', keyPath: "indexImage", unique: false} + ] + } + ]) + return rtn; + } + + public async login(data:any) { + let rtn:MyResponse = new MyResponse; + if(data.password == "123456" && data.username == "admin"){ + rtn.data = { + token:{tokenValue:"mockToken", tokenName:"name"}, + userinfo:{nickname:"nickname", username:data.username, id:1} + } + }else{ + rtn.data = null + rtn.msg = "admin 和 123456" + } + return rtn; + } + + public async logout() { + let rtn:MyResponse = new MyResponse; + return rtn; + } + + public async checkToken(data: any) { + let rtn:MyResponse = new MyResponse; + console.log("CheckToken: " + data.token) + rtn.data = { + token:{tokenValue:"mockToken", tokenName:"name"}, + userinfo:{nickname:"nickname", username:data.username, id:1} + } + return rtn; + } + + public async projectList(data:any){ + let rtn:MyResponse = new MyResponse; + const db:DBSelect = await onDBSelect(PROJECT_TABLE, DB_VER) + const r:any = await db.getAll() + rtn.data = [] + r.map(function (item: any) { + let url = "" + if(item.indexImage){ + const Url = URL || window.URL || window.webkitURL + url = Url.createObjectURL(item.indexImage) + } + rtn.data.push({ + id: item.projectId, + title: item.projectName, + release: item.release == 1, + label:item.remarks, + image:url + }) + }) + return rtn; + } + + public async createProject(data: any){ + let rtn:MyResponse = new MyResponse; + const db:DBSelect = await onDBSelect(PROJECT_TABLE, DB_VER) + rtn.data.id = await db.add({ projectName:data.projectName }) + return rtn; + } + + public async fetchProject(data: any){ + let rtn:MyResponse = new MyResponse; + const db:DBSelect = await onDBSelect(PROJECT_TABLE, DB_VER) + const r:any = await db.get(parseInt(data.projectId)) + rtn.data = { + id:r.projectId, + projectName: r.projectName, + state: r.release, + remarks: r.remarks, + content: r.content + } + return rtn; + } + + public async updateProject(data: any){ + let rtn:MyResponse = new MyResponse; + const db:DBSelect = await onDBSelect(PROJECT_TABLE, DB_VER) + const row:any = await db.get(parseInt(data.projectId)) + if("content" in data) row.content = data.content + if("projectName" in data) row.projectName = data.projectName + if("release" in data) row.release = data.release + if("remarks" in data) row.remarks = data.remarks + if("object" in data) { + row.indexImage = await fileToBlob(data.object) + } + await db.put(row) + return rtn; + } + + public async copyProject(data: any){ + let rtn:MyResponse = new MyResponse; + const db:DBSelect = await onDBSelect(PROJECT_TABLE, DB_VER) + const row:any = await db.get(parseInt(data.copyId)) + rtn.data.id =await db.add({ + projectName:data.projectName, + content:row.content, + indexImage:row.indexImage, + remarks:row.remarks + }) + return rtn; + } + + public async deleteProject(data: any){ + let rtn:MyResponse = new MyResponse; + const db:DBSelect = await onDBSelect(PROJECT_TABLE, DB_VER) + await db.del(parseInt(data.projectId)) + return rtn; + } + + public async changeProjectRelease(data: any){ + let rtn:MyResponse = new MyResponse; + return rtn; + } + + public async uploadFile(data: File, params:any){ + // Todo: 图片可以保存在表中 + let rtn:MyResponse = new MyResponse; + rtn.data.uri = fileToUrl(data) + return rtn; + } + + public getFileUrl(uploadUri:string){ + return uploadUri; + } +} + diff --git a/src/api/axios.config.ts b/src/backend/java/axios.config.ts similarity index 80% rename from src/api/axios.config.ts rename to src/backend/java/axios.config.ts index 5c976c7c..70b209e9 100644 --- a/src/api/axios.config.ts +++ b/src/backend/java/axios.config.ts @@ -1,4 +1,7 @@ -import { ModuleTypeEnum } from '@/enums/httpEnum' +export enum ModuleTypeEnum { + SYSTEM = 'sys', + PROJECT = 'project', +} // 接口白名单(免登录) export const fetchAllowList = [ diff --git a/src/backend/java/axios.ts b/src/backend/java/axios.ts new file mode 100644 index 00000000..bb5b0269 --- /dev/null +++ b/src/backend/java/axios.ts @@ -0,0 +1,71 @@ +import axios, { AxiosResponse, AxiosRequestConfig } from 'axios' +import { ResultEnum } from "@/enums/httpEnum" +import { PageEnum, ErrorPageNameMap } from "@/enums/pageEnum" +import { StorageEnum } from '@/enums/storageEnum' +import { axiosPre } from '@/settings/httpSetting' +import { SystemStoreEnum, SystemStoreUserInfoEnum } from '@/store/modules/systemStore/systemStore.d' +import { redirectErrorPage, getLocalStorage, routerTurnByName, httpErrorHandle } from '@/utils' +import { fetchAllowList } from './axios.config' +import includes from 'lodash/includes' + +const axiosInstance = axios.create({ + baseURL: `${import.meta.env.PROD ? import.meta.env.VITE_PRO_PATH : ''}${axiosPre}`, + timeout: ResultEnum.TIMEOUT, +}) + +axiosInstance.interceptors.request.use( + (config: AxiosRequestConfig) => { + // 白名单校验 + if (includes(fetchAllowList, config.url)) return config + // 获取 token + const info = getLocalStorage(StorageEnum.GO_SYSTEM_STORE) + // 重新登录 + if (!info) { + routerTurnByName(PageEnum.BASE_LOGIN_NAME) + return config + } + const userInfo = info[SystemStoreEnum.USER_INFO] + config.headers = { + ...config.headers, + [userInfo[SystemStoreUserInfoEnum.TOKEN_NAME] || 'token']: userInfo[SystemStoreUserInfoEnum.USER_TOKEN] || '' + } + return config + }, + (err: AxiosRequestConfig) => { + Promise.reject(err) + } +) + +// 响应拦截器 +axiosInstance.interceptors.response.use( + (res: AxiosResponse) => { + const { code } = res.data as { code: number } + + // 成功 + if (code === ResultEnum.SUCCESS) { + return Promise.resolve(res.data) + } + + // 登录过期 + if (code === ResultEnum.TOKEN_OVERDUE) { + window['$message'].error(window['$t']('http.token_overdue_message')) + routerTurnByName(PageEnum.BASE_LOGIN_NAME) + return Promise.resolve(res.data) + } + + // 固定错误码重定向 + if (ErrorPageNameMap.get(code)) { + redirectErrorPage(code) + return Promise.resolve(res.data) + } + + // 提示错误 + window['$message'].error(window['$t']((res.data as any).msg)) + return Promise.resolve(res.data) + }, + (err: AxiosResponse) => { + Promise.reject(err) + } +) + +export default axiosInstance diff --git a/src/backend/java/http.ts b/src/backend/java/http.ts new file mode 100644 index 00000000..534f0c0e --- /dev/null +++ b/src/backend/java/http.ts @@ -0,0 +1,226 @@ +import axiosInstance from './axios' +import { + RequestHttpEnum, + ContentTypeEnum, + RequestBodyEnum, + RequestDataTypeEnum, + RequestContentTypeEnum, + RequestParamsObjType +} from '@/enums/httpEnum' +import type { RequestGlobalConfigType, RequestConfigType } from '@/store/modules/chartEditStore/chartEditStore.d' + +export const get = (url: string, params?: object) => { + return axiosInstance({ + url: url, + method: RequestHttpEnum.GET, + params: params, + }) +} + +export const post = (url: string, data?: object, headersType?: string) => { + return axiosInstance({ + url: url, + method: RequestHttpEnum.POST, + data: data, + headers: { + 'Content-Type': headersType || ContentTypeEnum.JSON + } + }) +} + +export const patch = (url: string, data?: object, headersType?: string) => { + return axiosInstance({ + url: url, + method: RequestHttpEnum.PATCH, + data: data, + headers: { + 'Content-Type': headersType || ContentTypeEnum.JSON + } + }) +} + +export const put = (url: string, data?: object, headersType?: ContentTypeEnum) => { + return axiosInstance({ + url: url, + method: RequestHttpEnum.PUT, + data: data, + headers: { + 'Content-Type': headersType || ContentTypeEnum.JSON + } + }) +} + +export const del = (url: string, params?: object) => { + return axiosInstance({ + url: url, + method: RequestHttpEnum.DELETE, + params + }) +} + +// 获取请求函数,默认get +export const http = (type?: RequestHttpEnum) => { + switch (type) { + case RequestHttpEnum.GET: + return get + + case RequestHttpEnum.POST: + return post + + case RequestHttpEnum.PATCH: + return patch + + case RequestHttpEnum.PUT: + return put + + case RequestHttpEnum.DELETE: + return del + + default: + return get + } +} +const prefix = 'javascript:' +// 对输入字符进行转义处理 +export const translateStr = (target: string | object) => { + if (typeof target === 'string') { + if (target.startsWith(prefix)) { + const funcStr = target.split(prefix)[1] + let result; + try { + result = new Function(`${funcStr}`)() + } catch (error) { + console.log(error) + window['$message'].error('js内容解析有误!') + } + return result + } else { + return target + } + } + for (const key in target) { + if (Object.prototype.hasOwnProperty.call(target, key)) { + const subTarget = (target as any)[key]; + (target as any)[key] = translateStr(subTarget) + } + } + return target +} + +/** + * * 自定义请求 + * @param targetParams 当前组件参数 + * @param globalParams 全局参数 + */ +export const customizeHttp = (targetParams: RequestConfigType, globalParams: RequestGlobalConfigType) => { + if (!targetParams || !globalParams) { + return + } + + // 全局 + const { + // 全局请求源地址 + requestOriginUrl, + // 全局请求内容 + requestParams: globalRequestParams + } = globalParams + + // 目标组件(优先级 > 全局组件) + const { + // 请求地址 + requestUrl, + // 普通 / sql + requestContentType, + // 获取数据的方式 + requestDataType, + // 请求方式 get/post/del/put/patch + requestHttpType, + // 请求体类型 none / form-data / x-www-form-urlencoded / json /xml + requestParamsBodyType, + // SQL 请求对象 + requestSQLContent, + // 请求内容 params / cookie / header / body: 同 requestParamsBodyType + requestParams: targetRequestParams + } = targetParams + + // 静态排除 + if (requestDataType === RequestDataTypeEnum.STATIC) return + + if (!requestUrl) { + return + } + + // 处理头部 + let headers: RequestParamsObjType = { + ...globalRequestParams.Header, + ...targetRequestParams.Header + } + headers = translateStr(headers) + + // data 参数 + let data: RequestParamsObjType | FormData | string = {} + // params 参数 + let params: RequestParamsObjType = { ...targetRequestParams.Params } + params = translateStr(params) + // form 类型处理 + let formData: FormData = new FormData() + formData.set('default', 'defaultData') + // 类型处理 + + switch (requestParamsBodyType) { + case RequestBodyEnum.NONE: + break + + case RequestBodyEnum.JSON: + headers['Content-Type'] = ContentTypeEnum.JSON + data = translateStr(JSON.parse(targetRequestParams.Body['json'])) + // json 赋值给 data + break + + case RequestBodyEnum.XML: + headers['Content-Type'] = ContentTypeEnum.XML + // xml 字符串赋值给 data + data = translateStr(targetRequestParams.Body['xml']) + break + + case RequestBodyEnum.X_WWW_FORM_URLENCODED: { + headers['Content-Type'] = ContentTypeEnum.FORM_URLENCODED + const bodyFormData = targetRequestParams.Body['x-www-form-urlencoded'] + for (const i in bodyFormData) formData.set(i, translateStr(bodyFormData[i])) + // FormData 赋值给 data + data = formData + break + } + + case RequestBodyEnum.FORM_DATA: { + headers['Content-Type'] = ContentTypeEnum.FORM_DATA + const bodyFormUrlencoded = targetRequestParams.Body['form-data'] + for (const i in bodyFormUrlencoded) { + formData.set(i, translateStr(bodyFormUrlencoded[i])) + } + // FormData 赋值给 data + data = formData + break + } + } + + // sql 处理 + if (requestContentType === RequestContentTypeEnum.SQL) { + headers['Content-Type'] = ContentTypeEnum.JSON + data = requestSQLContent + } + + try { + const url = (new Function("return `" + `${requestOriginUrl}${requestUrl}`.trim() + "`"))(); + return axiosInstance({ + url, + method: requestHttpType, + data, + params, + headers + }) + } catch (error) { + console.log(error) + window['$message'].error('URL地址格式有误!') + } +} diff --git a/src/backend/mock/mockbackend.ts b/src/backend/mock/mockbackend.ts new file mode 100644 index 00000000..2c1ae819 --- /dev/null +++ b/src/backend/mock/mockbackend.ts @@ -0,0 +1,130 @@ +import { MyResponse, IBackend } from './ibackend' +import { fileToUrl } from '@/utils' + + +/** + * MockBackend + * 模拟纯前端,不会保存,也不报错。 + */ + +export class MockBackend implements IBackend { + public async init(data: any) { + let rtn:MyResponse = new MyResponse; + return rtn; + } + + public async login(data:any) { + let rtn:MyResponse = new MyResponse; + if(data.password == "123456" && data.username == "admin"){ + rtn.data = { + token:{tokenValue:"mockToken", tokenName:"name"}, + userinfo:{nickname:"nickname", username:data.username, id:1} + } + }else{ + rtn.data = null + rtn.msg = "用户名或密码错误!" + } + return rtn; + } + + public async logout() { + let rtn:MyResponse = new MyResponse; + return rtn; + } + + public async checkToken(data:any){ + let rtn:MyResponse = new MyResponse; + return rtn; + } + + public async projectList(data:any){ + let rtn:MyResponse = new MyResponse; + rtn.data =[ + { + id: 1, + title: '假数据不可用', + release: true, + label: '官方案例' + }, + { + id: 2, + title: '物料2-假数据不可用', + release: false, + label: '官方案例' + }, + { + id: 3, + title: '物料3-假数据不可用', + release: false, + label: '官方案例' + }, + { + id: 4, + title: '物料4-假数据不可用', + release: false, + label: '官方案例' + }, + { + id: 5, + title: '物料5-假数据不可用', + release: false, + label: '官方案例' + } + ]; + return rtn; + } + + public async createProject(data: any){ + let rtn:MyResponse = new MyResponse; + rtn.data.id = "newId" + return rtn; + } + + public async fetchProject(data: any){ + let rtn:MyResponse = new MyResponse; + rtn.data = { + id:data.projectId, + projectName: '假数据不可用', + indexImage:'', + state: 0, + remarks: '官方案例', + content: null + } + return rtn; + } + + public async saveProject(data: object){ + let rtn:MyResponse = new MyResponse; + return rtn; + } + + public async updateProject(data: any){ + let rtn:MyResponse = new MyResponse; + return rtn; + } + + public async copyProject(data: any){ + let rtn:MyResponse = new MyResponse; + return rtn; + } + + public async deleteProject(data: any){ + let rtn:MyResponse = new MyResponse; + return rtn; + } + + public async changeProjectRelease(data: any){ + let rtn:MyResponse = new MyResponse; + return rtn; + } + + public async uploadFile(data: File, params:any){ + let rtn:MyResponse = new MyResponse; + rtn.data.uri = fileToUrl(data) + return rtn; + } + + public getFileUrl(uploadUri:string){ + return uploadUri; + } +} \ No newline at end of file diff --git a/src/enums/httpEnum.ts b/src/enums/httpEnum.ts index effdfd76..23109e03 100644 --- a/src/enums/httpEnum.ts +++ b/src/enums/httpEnum.ts @@ -1,10 +1,6 @@ -// 模块 Path 前缀分类 -export enum ModuleTypeEnum { - SYSTEM = 'sys', - PROJECT = 'project', -} - -// 请求结果集 +/** + * @description: 请求结果集 + */ export enum ResultEnum { DATA_SUCCESS = 0, SUCCESS = 200, @@ -12,7 +8,7 @@ export enum ResultEnum { SERVER_FORBIDDEN = 403, NOT_FOUND = 404, TOKEN_OVERDUE = 886, - TIMEOUT = 60000, + TIMEOUT = 60000 } // 数据相关 @@ -37,7 +33,9 @@ export enum RequestHttpHeaderEnum { COOKIE = 'Cookie' } -// 请求方法 +/** + * @description: 请求方法 + */ export enum RequestHttpEnum { GET = 'get', POST = 'post', @@ -118,7 +116,9 @@ export type RequestParams = { } } -// 常用的contentTyp类型 +/** + * @description: 常用的contentTyp类型 + */ export enum ContentTypeEnum { // json JSON = 'application/json;charset=UTF-8', diff --git a/src/enums/storageEnum.ts b/src/enums/storageEnum.ts index a36e99e8..b0818a24 100644 --- a/src/enums/storageEnum.ts +++ b/src/enums/storageEnum.ts @@ -1,8 +1,10 @@ export enum StorageEnum { // 全局设置 - GO_SETTING_STORE = 'GO_SETTING', + GO_SYSTEM_SETTING_STORE = 'GO_SYSTEM_SETTING', + // token 等信息 + GO_ACCESS_TOKEN_STORE = 'GO_ACCESS_TOKEN', // 登录信息 - GO_SYSTEM_STORE = 'GO_SYSTEM', + GO_LOGIN_INFO_STORE = 'GO_LOGIN_INFO', // 语言 GO_LANG_STORE = 'GO_LANG', // 当前选择的主题 diff --git a/src/hooks/index.ts b/src/hooks/index.ts index c1d5037b..bc9825fb 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -2,4 +2,4 @@ export * from '@/hooks/useTheme.hook' export * from '@/hooks/usePreviewScale.hook' export * from '@/hooks/useCode.hook' export * from '@/hooks/useChartDataFetch.hook' -export * from '@/hooks/useSystemInit.hook' \ No newline at end of file +export * from '@/hooks/useLifeHandler.hook' \ No newline at end of file diff --git a/src/hooks/useLifeHandler.hook.ts b/src/hooks/useLifeHandler.hook.ts new file mode 100644 index 00000000..484031de --- /dev/null +++ b/src/hooks/useLifeHandler.hook.ts @@ -0,0 +1,280 @@ +import { CreateComponentType, EventLife } from '@/packages/index.d' +import * as echarts from 'echarts' +import { BackEndFactory } from '@/backend/ibackend' +import { reactive, toRef , watch, computed} from 'vue'; + +/** + * 事件测试: + * +切换显示名称为 饼图 和 柱状图 的图标 +const range = runtime.fn.selectComponents("饼图 柱状图") +const h = runtime.fn.getChartConfig(range, "hide") +runtime.fn.setChartConfig(range, "hide", !h) + +修改一个名称 柱状图001 组件id 2wolqibrx3c000 的图表数据,以下两句等效 +runtime.fn.setChartConfig("柱状图001", "dataset", {"dimensions":["product","data1","data2"],"source":[{"product":"Mon","data1":120,"data2":130}]}) +runtime.fn.setChartConfig("#2wolqibrx3c000", "dataset", {"dimensions":["product","data1","data2"],"source":[{"product":"Mon","data1":120,"data2":230}]}) + +找到一个组并隐藏 +const c = runtime.fn.selectOneComponent("分组") +if(c){ + console.log(runtime.fn.getChartConfig(c, "isGroup" )) + runtime.fn.setChartConfig(c, "hide", true) +} + +调用组件 exposed 函数的例子 +组件中增加: defineExpose({ actionTest:actionTest }) +以下调用名称为 柱状图 组件的 actionTest +runtime.fn.callExposed("柱状图", "actionTest") + + +数据驱动界面: +图表A 的 MOUNTED 加入对 status1 的 Watch, = "0" 隐藏 +watch(()=>runtime.variables.status1, newValue => runtime.fn.setChartConfig(this, "hide", newValue == "0")) +图表B 的 MOUNTED 也加入对 status1 的 Watch = "1" 隐藏 +watch(()=>runtime.variables.status1, newValue => runtime.fn.setChartConfig(this, "hide", newValue == "1")) +点击事件代码,实现图表A 和 图表B 的切换显示: +if(runtime.variables.status1 == "0"){ + runtime.variables.status1 = "1" +} else{ + runtime.variables.status1 = "0" +} + +图表A 的 MOUNTED 加入对 data1 的 Watch +watch(()=>runtime.datasets.data1, +newValue => runtime.fn.setChartConfig(this, "dataset", newValue)) +图表B 的 MOUNTED 加入对 data1 的 Watch +watch(()=>runtime.datasets.data1, +newValue => runtime.fn.setChartConfig(this, "dataset", newValue)) +点击事件代码,修改datasets.data1,同时更新图表A 和 图表B 的数据 : +runtime.datasets.data1 = {"dimensions":["product","data1","data2"],"source":[{"product":"Mon","data1":120,"data2":230}]} + + * + */ + + + + + +// * 初始化 +export const useSystemInit = async () => { + const res = await BackEndFactory.init({}) as any; +} + +const getOneChartConfig = (component:any, configName:string, params?:any)=>{ + let root = null + if(component.proxy.chartConfig) root = component.proxy.chartConfig + else if (component.proxy.groupData) root = component.proxy.groupData + // if(!root) return null + switch(configName){ + case "hide": + return root.status.hide + break; + case "dataset": + return root.option.dataset + break; + case "isGroup": + return root.isGroup + break; + case "key": + return root.key + break; + case "attr": + return root.attr + break; + case "name": + return root.chartConfig.title + } +} + +const setOneChartConfig = (component:any, configName:string, newValue:any, params?:any)=>{ + let root = null + if(component.proxy.chartConfig) root = component.proxy.chartConfig + else if (component.proxy.groupData) root = component.proxy.groupData + switch(configName){ + case "hide": + root.status.hide = newValue + break; + case "dataset": + root.option.dataset = newValue + break; + } +} + + +/** + * 选择器语法:参考 css selectors + * 名称 组件名称,不能有空格和特殊字符(. # 引号等) + * [name=名称] Todo + * #id 组件编号 + * .key 组件类型 Todo + * @param selectors + * @returns [] + */ +const getComponentsBySelectors = (selectors:string):any[]=>{ + // 返回:数组,可能多个 + let rtn:any[] = [] + const ar = selectors.split(" ") + for(let a of ar){ + rtn = rtn.concat(getComponentsBySelector(a)) + } + return rtn +} + +const getComponentsBySelector = (selector:string):any[]=>{ + // 返回:数组,可能多个 + const rtn:any[] = [] + if(selector.substring(0,1) == "#") + { + const key = selector.substring(1) + if(key in components){ + return [components[key]] + } + return rtn + } + for (let key in components) { + if(getOneChartConfig(components[key], "name") == selector){ + rtn.push(components[key]) + } + } + return rtn +} + + +// 所有图表组件集合对象 +const components: { [K in string]?: any } = {} + +const runtime = { + // 变量,管理各种状态 + variables:reactive({}), + // 数据集 + datasets:reactive({}), + // 组件列表 {} + components:components, + // 帮助类 + fn:{ + /** + * 选择一个组件 + * @param selectors string 选择器语法 | component | [component] + * @return 第一个符合要求的 component 或 null + */ + selectOneComponent:(selectors:any)=>{ + const cList = runtime.fn.selectComponents(selectors) + if(cList.length > 0){ + return cList[0] + } + return null + }, + /** + * 选择组件 + * @param selectors string 选择器语法 | component | [component] + * @return 要求的 [component] 或 [] + */ + selectComponents:(selectors:any):any[]=>{ + if(!selectors) return [] + if(typeof selectors == "string") return getComponentsBySelectors(selectors) + if(Array.isArray(selectors)) return selectors + return [selectors] + }, + /** + * 获取组件的值,如果多个,使用第一个 + * @param selectors string 选择器语法 | component | [component] + * @param configName 配置名称 + * @param params 备用参数,可选 + * @returns 配置的值 + */ + getChartConfig:(selectors:any, configName:string, params?:any)=>{ + const component:any = runtime.fn.selectOneComponent(selectors) + if(!component && !component.proxy) return null + return getOneChartConfig(component, configName, params) + }, + /** + * 设置组件的值,支持多个 + * @param selectors string 选择器语法 | component | [component] + * @param configName 配置名称 + * @param newValue 新值 + * @param params 备用参数,可选 + * @returns 配置的值 + */ + setChartConfig:(selectors:any, configName:string, newValue:any, params?:any)=>{ + const cList:any[] = runtime.fn.selectComponents(selectors) + for(let c of cList){ + if(!c && !c.proxy) return null + setOneChartConfig(c, configName, newValue, params) + } + }, + /** + * 调用组件暴露的函数,组件中使用 defineExpose 进行定义 + * @param selectors string 选择器语法 | component | [component] + * @param action 组件中 defineExpose 的函数名 + * @param params 调用的参数只支持一个参数或没有参数 + * @returns 无 + */ + callExposed:(selectors:any, action:string, params?:any)=>{ + const cList:any[] = runtime.fn.selectComponents(selectors) + for(let c of cList){ + if(!c && !c.exposed) return null + if(typeof c.exposed[action] == "function") c.exposed[action](params) + } + } + } +} + +// 项目提供的npm 包变量 +export const npmPkgs = { echarts, toRef , watch, computed, runtime } + +export const useLifeHandler = (chartConfig: CreateComponentType) => { + const events = chartConfig.events || {} + console.log("chartConfig.events") + console.log(chartConfig.events) + // 生成生命周期事件 + let lifeEvents = { + [EventLife.BEFORE_MOUNT](e: any) { + // 存储组件 + components[chartConfig.id] = e.component + const fnStr = (events[EventLife.BEFORE_MOUNT] || '').trim() + generateFunc(fnStr, e, e.component) + }, + [EventLife.MOUNTED](e: any) { + const fnStr = (events[EventLife.MOUNTED] || '').trim() + generateFunc(fnStr, e, e.component) + } + } + // 遍历,按需侦听 + for(let key in EventLife) + { + if(key != "BEFORE_MOUNT" && key != "MOUNTED"){ + const k = EventLife[key as keyof typeof EventLife] + const fnStr = (events[k] || '').trim() + if(fnStr){ + lifeEvents[k as keyof typeof lifeEvents] = (e:any) => { + const fnStr = (events[k] || '').trim() + generateFunc(fnStr, e, components[chartConfig.id]) + } + } + } + } + return lifeEvents +} + +/** + * + * @param fnStr 用户方法体代码 + * @param e 执行生命周期的动态组件实例 + */ +function generateFunc(fnStr: string, e: any, component:any) { + if(fnStr == "") return + try { + // npmPkgs 便于拷贝 echarts 示例时设置option 的formatter等相关内容 + Function(` + "use strict"; + return ( + async function(e, components, node_modules){ + const {${Object.keys(npmPkgs).join()}} = node_modules; + ${fnStr} + } + )`)().bind(component)(e, components, npmPkgs) + } catch (error) { + console.error(error) + } +} diff --git a/src/hooks/useSystemInit.hook.ts b/src/hooks/useSystemInit.hook.ts deleted file mode 100644 index 72f2d0fa..00000000 --- a/src/hooks/useSystemInit.hook.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useSystemStore } from '@/store/modules/systemStore/systemStore' -import { SystemStoreEnum } from '@/store/modules/systemStore/systemStore.d' -import { ResultEnum } from '@/enums/httpEnum' -import { ossUrlApi } from '@/api/path/' - - -// * 初始化 -export const useSystemInit = async () => { - const systemStore = useSystemStore() - - // 获取 OSS 信息 - const getOssUrl = async () => { - const res = await ossUrlApi({}) as unknown as MyResponseType - if (res.code === ResultEnum.SUCCESS) { - systemStore.setItem(SystemStoreEnum.FETCH_INFO, { - OSSUrl: res.data?.bucketURL - }) - } - } - - // 执行 - getOssUrl() -} \ No newline at end of file diff --git a/src/packages/index.d.ts b/src/packages/index.d.ts index 5e800fe9..336b584c 100644 --- a/src/packages/index.d.ts +++ b/src/packages/index.d.ts @@ -90,6 +90,21 @@ export const BlendModeEnumList = [ { label: '亮度', value: 'luminosity' } ] +// vue3 生命周期事件 +export enum EventLife { + // 渲染之后 + MOUNTED = 'vnodeMounted', + // 渲染之前 + BEFORE_MOUNT = 'vnodeBeforeMount', + // 鼠标事件 + MOUSE_CLICK = 'click', + MOUSE_OVER = "mouseover", + MOUSE_LEAVE = "mouseleave", + // 图表事件 + ECHART_LEGEND_SELECT_CHANGED = "legendselectchanged", + ECHART_HIGH_LIGHT = "highlight" +} + // 组件实例类 export interface PublicConfigType { id: string @@ -115,6 +130,9 @@ export interface PublicConfigType { } filter?: string status: StatusType + events?: { + [K in EventLife]?: string + } } export interface CreateComponentType extends PublicConfigType, requestConfig { diff --git a/src/packages/public/publicConfig.ts b/src/packages/public/publicConfig.ts index 3924834a..ca20e464 100644 --- a/src/packages/public/publicConfig.ts +++ b/src/packages/public/publicConfig.ts @@ -81,6 +81,8 @@ export class PublicConfigClass implements PublicConfigType { public request = cloneDeep(requestConfig) // 数据过滤 public filter = undefined + // 事件 + public events = undefined } // 多选成组类 diff --git a/src/router/router-guards.ts b/src/router/router-guards.ts index bdaa7fb2..d3c8858d 100644 --- a/src/router/router-guards.ts +++ b/src/router/router-guards.ts @@ -18,11 +18,13 @@ export function createRouterGuards(router: Router) { const isErrorPage = router.getRoutes().findIndex((item) => item.name === to.name); if (isErrorPage === -1) { next({ name: PageEnum.ERROR_PAGE_NAME_404 }) + return } // @ts-ignore if (!routerAllowList.includes(to.name) && !loginCheck()) { next({ name: PageEnum.BASE_LOGIN_NAME }) + return } next() }) diff --git a/src/store/modules/settingStore/settingStore.ts b/src/store/modules/settingStore/settingStore.ts index 9e653119..4a90f7ae 100644 --- a/src/store/modules/settingStore/settingStore.ts +++ b/src/store/modules/settingStore/settingStore.ts @@ -4,10 +4,10 @@ import { asideCollapsedWidth } from '@/settings/designSetting' import { SettingStoreType, ToolsStatusEnum } from './settingStore.d' import { setLocalStorage, getLocalStorage } from '@/utils' import { StorageEnum } from '@/enums/storageEnum' -const { GO_SETTING_STORE } = StorageEnum +const { GO_SYSTEM_SETTING_STORE } = StorageEnum const storageSetting: SettingStoreType = getLocalStorage( - GO_SETTING_STORE + GO_SYSTEM_SETTING_STORE ) // 全局设置 @@ -48,7 +48,7 @@ export const useSettingStore = defineStore({ this.$patch(state => { state[key] = value }) - setLocalStorage(GO_SETTING_STORE, this.$state) + setLocalStorage(GO_SYSTEM_SETTING_STORE, this.$state) } } }) diff --git a/src/utils/file.ts b/src/utils/file.ts index 4a10bd38..9224bee2 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -25,6 +25,23 @@ export const base64toFile = (dataurl: string, fileName: string) => { return ImageUrl } +/** + * file转 blob + * @param { File } file 文件对象 + */ +export const fileToBlob = (file:File) =>{ + return new Promise(function (resolve, reject) { + let reader = new FileReader() + reader.readAsArrayBuffer(file) + reader.onload = function (e: ProgressEvent) { + if(e.target){ + const blob = new Blob([e.target.result], { type: file.type }); + resolve(blob); + } + } + }) +} + /** * * url转file */ diff --git a/src/utils/http.ts b/src/utils/http.ts index 8e92db7b..41a92adf 100644 --- a/src/utils/http.ts +++ b/src/utils/http.ts @@ -1,6 +1,27 @@ /** - * * 请求失败统一处理 + * 请求失败统一处理,allowRoute 允许跳转。 + * @param MyResponse MyResponseType,可以为空。 + * @return */ -export const httpErrorHandle = () => { + import { ResultEnum } from "@/enums/httpEnum" + import { PageEnum, ErrorPageNameMap } from "@/enums/pageEnum" + import { redirectErrorPage, routerTurnByName } from '@/utils' + + export const httpErrorHandle = (MyResponse?:any, allowRoute:boolean = true) => { + if(MyResponse){ + const {code, msg} = MyResponse + if (MyResponse.code === ResultEnum.TOKEN_OVERDUE) { + window['$message'].error(msg || window['$t']('http.token_overdue_message')) + if(allowRoute) routerTurnByName(PageEnum.BASE_LOGIN_NAME) + return + } + + if (MyResponse.code != ResultEnum.SUCCESS) { + // 其他错误处理 Todo + if (ErrorPageNameMap.get(code) && allowRoute) { + redirectErrorPage(code) + } + } + } window['$message'].error(window['$t']('http.error_message')) } \ No newline at end of file diff --git a/src/utils/router.ts b/src/utils/router.ts index 5f104f56..22bd6d95 100644 --- a/src/utils/router.ts +++ b/src/utils/router.ts @@ -2,11 +2,10 @@ import { useRoute } from 'vue-router' import { ResultEnum, RequestHttpHeaderEnum } from '@/enums/httpEnum' import { ErrorPageNameMap, PageEnum, PreviewEnum } from '@/enums/pageEnum' import { docPath, giteeSourceCodePath } from '@/settings/pathConst' -import { SystemStoreEnum, SystemStoreUserInfoEnum } from '@/store/modules/systemStore/systemStore.d' import { StorageEnum } from '@/enums/storageEnum' import { clearLocalStorage, getLocalStorage, clearCookie } from './storage' import router from '@/router' -import { logoutApi } from '@/api/path' +import { BackEndFactory } from '@/backend/ibackend' /** * * 根据名字跳转路由 @@ -106,11 +105,11 @@ export const reloadRoutePage = () => { */ export const logout = async () => { try { - const res = await logoutApi() as unknown as MyResponseType + const res = await BackEndFactory.logout() as any if(res.code === ResultEnum.SUCCESS) { window['$message'].success(window['$t']('global.logout_success')) clearCookie(RequestHttpHeaderEnum.COOKIE) - clearLocalStorage(StorageEnum.GO_SYSTEM_STORE) + clearLocalStorage(StorageEnum.GO_LOGIN_INFO_STORE) routerTurnByName(PageEnum.BASE_LOGIN_NAME) } } catch (error) { @@ -147,7 +146,8 @@ export const openGiteeSourceCode = () => { * @returns boolean */ export const isPreview = () => { - return document.location.hash.includes('preview') + return false + //return document.location.hash.includes('preview') } /** @@ -163,6 +163,15 @@ export const fetchRouteParams = () => { } } +export const fetchRouteQuery = () => { + try { + const route = useRoute() + return route.query + } catch (error) { + window['$message'].warning('查询路由信息失败,请联系管理员!') + } +} + /** * * 通过硬解析获取当前路由下的参数 * @returns object @@ -188,18 +197,17 @@ export const goHome = () => { * * 判断是否登录 * @return boolean */ -export const loginCheck = () => { + export const loginCheck = () => { try { - const info = getLocalStorage(StorageEnum.GO_SYSTEM_STORE) + const info = getLocalStorage(StorageEnum.GO_LOGIN_INFO_STORE) if (!info) return false - if (info[SystemStoreEnum.USER_INFO][SystemStoreUserInfoEnum.USER_TOKEN]) { - return true - } + // 检查 Token ? + if(info.token && info.userinfo) return true return false } catch (error) { return false } -} +} /** * * 预览地址 diff --git a/src/views/chart/ContentConfigurations/components/CanvasPage/index.vue b/src/views/chart/ContentConfigurations/components/CanvasPage/index.vue index 75634702..a7730a53 100644 --- a/src/views/chart/ContentConfigurations/components/CanvasPage/index.vue +++ b/src/views/chart/ContentConfigurations/components/CanvasPage/index.vue @@ -30,7 +30,7 @@ :onBeforeUpload="beforeUploadHandle" > - 背景 + 背景
@@ -131,20 +131,19 @@ import { backgroundImageSize } from '@/settings/designSetting' import { FileTypeEnum } from '@/enums/fileTypeEnum' import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore' import { EditCanvasConfigEnum } from '@/store/modules/chartEditStore/chartEditStore.d' -import { useSystemStore } from '@/store/modules/systemStore/systemStore' import { StylesSetting } from '@/components/Pages/ChartItemSetting' import { UploadCustomRequestOptions } from 'naive-ui' import { fileToUrl, loadAsyncComponent, fetchRouteParamsLocation } from '@/utils' import { PreviewScaleEnum } from '@/enums/styleEnum' import { ResultEnum } from '@/enums/httpEnum' import { icon } from '@/plugins' -import { uploadFile} from '@/api/path' +import { BackEndFactory } from '@/backend/ibackend' + const { ColorPaletteIcon } = icon.ionicons5 const { ScaleIcon, FitToScreenIcon, FitToHeightIcon, FitToWidthIcon } = icon.carbon const chartEditStore = useChartEditStore() -const systemStore = useSystemStore() const canvasConfig = chartEditStore.getEditCanvasConfig const editCanvas = chartEditStore.getEditCanvas @@ -273,25 +272,12 @@ const clearColor = () => { const customRequest = (options: UploadCustomRequestOptions) => { const { file } = options nextTick(async () => { - if(!systemStore.getFetchInfo.OSSUrl) { - window['$message'].error('添加图片失败,请刷新页面重试!') - return - } if (file.file) { - // 修改名称 - const newNameFile = new File( - [file.file], - `${fetchRouteParamsLocation()}_index_background.png`, - { type: file.file.type } - ) - let uploadParams = new FormData() - uploadParams.append('object', newNameFile) - const uploadRes = await uploadFile(systemStore.getFetchInfo.OSSUrl ,uploadParams) as unknown as MyResponseType - + const uploadRes = await BackEndFactory.uploadFile(file.file, null) as any if(uploadRes.code === ResultEnum.SUCCESS) { chartEditStore.setEditCanvasConfig( EditCanvasConfigEnum.BACKGROUND_IMAGE, - uploadRes.data.objectContent.httpRequest.uri + uploadRes.data.uri ) chartEditStore.setEditCanvasConfig( EditCanvasConfigEnum.SELECT_COLOR, diff --git a/src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataRequest/index.vue b/src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataRequest/index.vue index ec0c1d37..3a016c79 100644 --- a/src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataRequest/index.vue +++ b/src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataRequest/index.vue @@ -1,5 +1,5 @@ @@ -27,7 +28,7 @@ import { PropType } from 'vue' import { CreateComponentGroupType } from '@/packages/index.d' import { animationsClass, getFilterStyle, getTransformStyle, getBlendModeStyle } from '@/utils' import { getSizeStyle, getComponentAttrStyle, getStatusStyle } from '../../utils' - +import { useLifeHandler } from '@/hooks' const props = defineProps({ groupData: { type: Object as PropType, diff --git a/src/views/preview/components/PreviewRenderList/index.vue b/src/views/preview/components/PreviewRenderList/index.vue index 4335ab96..16bc7e72 100644 --- a/src/views/preview/components/PreviewRenderList/index.vue +++ b/src/views/preview/components/PreviewRenderList/index.vue @@ -1,7 +1,7 @@