18 Commits
Author SHA1 Message Date
hanchaoboandGitea f58e15fe60 Merge branch 'hcb' of kn-cost/cost-frontend into dev 2024-09-14 17:20:15 +08:00
hanchaobo 64e6a9ce31 实际成本 2024-09-14 17:19:34 +08:00
hanchaoboandGitea babbee9c62 Merge branch 'hcb' of kn-cost/cost-frontend into dev 2024-08-30 16:57:24 +08:00
bwm 5f59baf186 部署 2024-08-30 16:56:07 +08:00
hanchaoboandGitea edf4894719 Merge branch 'hcb' of kn-cost/cost-frontend into dev 2024-08-14 08:59:28 +08:00
bwm 49855f198a 成本挖掘页面 2024-08-14 08:58:29 +08:00
hanchaoboandGitea 1fb7d94353 Merge branch 'hcb' of kn-cost/cost-frontend into dev 2024-07-26 16:58:37 +08:00
bwm c71e5a6b01 组件流程开发 2024-07-26 16:53:28 +08:00
hanchaoboandGitea 9c7223483a Merge branch 'hcb' of kn-cost/cost-frontend into dev 2024-07-10 09:44:53 +08:00
hanchaobo 0326018db6 测试组件 2024-07-10 09:43:43 +08:00
hanchaoboandGitea 44bca173aa Merge branch 'hcb' of kn-cost/cost-frontend into dev 2024-07-05 09:48:23 +08:00
hanchaobo 3a6620b4e9 组件 2024-07-05 09:47:19 +08:00
hanchaoboandGitea e3da2f821d Merge branch 'hcb' of kn-cost/cost-frontend into dev 2024-07-01 10:05:12 +08:00
hanchaobo e69cd3af6c 页面 2024-07-01 10:03:53 +08:00
hanchaoboandGitea 435b683cfc Merge branch 'hcb' of kn-cost/cost-frontend into dev 2024-06-12 09:51:42 +08:00
hanchaobo 8de5841fb5 小时费率 2024-06-12 09:50:18 +08:00
hanchaoboandGitea 8f036e9be7 Merge branch 'hcb' of kn-cost/cost-frontend into dev 2024-06-03 14:55:09 +08:00
hanchaobo b159ccd420 框架搭建 2024-06-03 14:52:58 +08:00
149 changed files with 11688 additions and 2 deletions
+46
View File
@@ -0,0 +1,46 @@
"use strict";
module.exports = {
types: [
{ value: "✨新增", name: "新增: 新的内容" },
{ value: "🐛修复", name: "修复: 修复一个Bug" },
{ value: "📝文档", name: "文档: 变更的只有文档" },
{ value: "💄格式", name: "格式: 空格, 分号等格式修复" },
{ value: "♻️重构", name: "重构: 代码重构,注意和特性、修复区分开" },
{ value: "⚡️性能", name: "性能: 提升性能" },
{ value: "✅测试", name: "测试: 添加一个测试" },
{ value: "🔧工具", name: "工具: 开发工具变动(构建、脚手架工具等)" },
{ value: "⏪回滚", name: "回滚: 代码回退" }
],
scopes: [
{ name: "leetcode" },
{ name: "javascript" },
{ name: "typescript" },
{ name: "Vue" },
{ name: "node" }
],
// it needs to match the value for field type. Eg.: 'fix'
/* scopeOverrides: {
fix: [
{name: 'merge'},
{name: 'style'},
{name: 'e2eTest'},
{name: 'unitTest'}
]
}, */
// override the messages, defaults are as follows
messages: {
type: "选择一种你的提交类型:",
scope: "选择一个scope (可选):",
// used if allowCustomScopes is true
customScope: "Denote the SCOPE of this change:",
subject: "短说明:\n",
body: '长说明,使用"|"换行(可选):\n',
breaking: "非兼容性说明 (可选):\n",
footer: "关联关闭的issue,例如:#31, #34(可选):\n",
confirmCommit: "确定提交说明?(yes/no)"
},
allowCustomScopes: true,
allowBreakingChanges: ["特性", "修复"],
// limit subject length
subjectLimit: 100
};
+14
View File
@@ -0,0 +1,14 @@
# 页面标题
VITE_APP_TITLE='Qomo Ai'
# 开发环境配置
VITE_APP_ENV='development'
# 子应用路径配置
VITE_APP_PATH='/layoutApp/newEnergy'
# 子应用名称
VITE_APP_NAME='newEnergy'
# 开发环境
VITE_APP_BASE_API=http://172.16.1.141:8082
+15
View File
@@ -0,0 +1,15 @@
# 页面标题
VITE_APP_TITLE='Qomo Ai'
# 生产环境配置
VITE_APP_ENV='production'
# 子应用路径配置
VITE_APP_PATH='/layoutApp/newEnergy'
# 子应用名称
VITE_APP_NAME='newEnergy'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS=gzip
+12
View File
@@ -0,0 +1,12 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": ["eslint:recommended", "plugin:vue/vue3-essential", "prettier "],
"parserOptions": {
"ecmaVersion": "latest"
},
"plugins": ["vue"],
"rules": {}
}
+23
View File
@@ -0,0 +1,23 @@
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
**/*.log
tests/**/coverage/
tests/e2e/reports
selenium-debug.log
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.local
package-lock.json
yarn.lock
+4
View File
@@ -0,0 +1,4 @@
{
"*.{js,jsx,vue}": ["prettier --write ."],
"*.md": ["prettier --write"]
}
+6
View File
@@ -0,0 +1,6 @@
# pnpm 配置
shamefully-hoist=true
auto-install-peers=true
strict-peer-dependencies=false
@qomo:registry=https://packages.aliyun.com/669a1b3bbedb1acc325c96c0/npm/npm-registry/
@qomo-platform:registry=https://packages.aliyun.com/669a1b3bbedb1acc325c96c0/npm/npm-registry/
+15
View File
@@ -0,0 +1,15 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "none",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid",
"rangeStart": 0,
"endOfLine": "crlf"
}
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2018 RuoYi
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-2
View File
@@ -1,2 +0,0 @@
# cost-frontend
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
extends: ["cz"],
rules: {}
};
+262
View File
File diff suppressed because one or more lines are too long
+215
View File
@@ -0,0 +1,215 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible" />
<meta content="webkit" name="renderer" />
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport" />
<link href="/favicon.ico" rel="icon" />
<title>QoMo 子应用</title>
<script>
window._CONFIG = {
domianURL: 'http://172.16.0.87:8095',
// 其他配置项...
};
</script>
<!--[if lt IE 11
]><script>
window.location.href = "/html/ie.html";
</script><!
[endif]-->
<style>
html,
body,
#subApp {
height: 100%;
margin: 0px;
padding: 0px;
}
.chromeframe {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}
#loader-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999999;
}
#loader {
display: block;
position: relative;
left: 50%;
top: 50%;
width: 150px;
height: 150px;
margin: -75px 0 0 -75px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #fff;
-webkit-animation: spin 2s linear infinite;
-ms-animation: spin 2s linear infinite;
-moz-animation: spin 2s linear infinite;
-o-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
z-index: 1001;
}
#loader:before {
content: "";
position: absolute;
top: 5px;
left: 5px;
right: 5px;
bottom: 5px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #fff;
-webkit-animation: spin 3s linear infinite;
-moz-animation: spin 3s linear infinite;
-o-animation: spin 3s linear infinite;
-ms-animation: spin 3s linear infinite;
animation: spin 3s linear infinite;
}
#loader:after {
content: "";
position: absolute;
top: 15px;
left: 15px;
right: 15px;
bottom: 15px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #fff;
-moz-animation: spin 1.5s linear infinite;
-o-animation: spin 1.5s linear infinite;
-ms-animation: spin 1.5s linear infinite;
-webkit-animation: spin 1.5s linear infinite;
animation: spin 1.5s linear infinite;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
#loader-wrapper .loader-section {
position: fixed;
top: 0;
width: 51%;
height: 100%;
background: #7171c6;
z-index: 1000;
-webkit-transform: translateX(0);
-ms-transform: translateX(0);
transform: translateX(0);
}
#loader-wrapper .loader-section.section-left {
left: 0;
}
#loader-wrapper .loader-section.section-right {
right: 0;
}
.loaded #loader-wrapper .loader-section.section-left {
-webkit-transform: translateX(-100%);
-ms-transform: translateX(-100%);
transform: translateX(-100%);
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.loaded #loader-wrapper .loader-section.section-right {
-webkit-transform: translateX(100%);
-ms-transform: translateX(100%);
transform: translateX(100%);
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.loaded #loader {
opacity: 0;
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.loaded #loader-wrapper {
visibility: hidden;
-webkit-transform: translateY(-100%);
-ms-transform: translateY(-100%);
transform: translateY(-100%);
-webkit-transition: all 0.3s 1s ease-out;
transition: all 0.3s 1s ease-out;
}
.no-js #loader-wrapper {
display: none;
}
.no-js h1 {
color: #222222;
}
#loader-wrapper .load_title {
font-family: "Open Sans";
color: #fff;
font-size: 19px;
width: 100%;
text-align: center;
z-index: 9999999999999;
position: absolute;
top: 60%;
opacity: 1;
line-height: 30px;
}
#loader-wrapper .load_title span {
font-weight: normal;
font-style: italic;
font-size: 13px;
color: #fff;
opacity: 0.5;
}
</style>
</head>
<body>
<div id="subApp"></div>
<script src="/src/main.js" type="module"></script>
</body>
</html>
+73
View File
@@ -0,0 +1,73 @@
{
"name": "project",
"version": "1.0.0",
"description": "QomoAi 子应用",
"author": "lqy",
"license": "MIT",
"type": "commonjs",
"scripts": {
"dev": "vite",
"build:prod": "vite build",
"build:stage": "vite build --mode staging",
"preview": "vite preview",
"prepare": "husky install",
"pre-commit": "lint-staged",
"commit": "git-cz"
},
"dependencies": {
"@element-plus/icons-vue": "2.3.1",
"@qomo-platform/core": "^1.0.151",
"@vueup/vue-quill": "1.2.0",
"@vueuse/core": "10.6.1",
"axios": "1.6.7",
"echarts": "^5.5.0",
"element-plus": "2.4.3",
"file-saver": "2.0.5",
"js-cookie": "3.0.5",
"jsencrypt": "3.3.2",
"less": "^4.2.0",
"md5": "^2.3.0",
"monaco-editor": "^0.50.0",
"nprogress": "0.2.0",
"pinia": "2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"postcss-pxtorem": "^6.1.0",
"prettier": "^3.2.5",
"uuid": "^10.0.0",
"vite-plugin-qiankun": "^1.0.15",
"vue": "3.3.9",
"vue-router": "4.2.5"
},
"devDependencies": {
"@commitlint/config-conventional": "^19.0.3",
"@vitejs/plugin-vue": "4.5.0",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/compiler-sfc": "3.3.9",
"autoprefixer": "^10.4.18",
"commitizen": "^4.3.0",
"commitlint": "^19.0.3",
"commitlint-config-cz": "^0.13.3",
"cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^7.0.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.22.0",
"husky": "^8.0.0",
"husky-init": "^8.0.0",
"lint-staged": "^15.2.2",
"postcss": "^8.4.35",
"sass": "1.69.5",
"tailwindcss": "^3.4.1",
"unplugin-auto-import": "0.17.1",
"unplugin-vue-setup-extend-plus": "1.0.0",
"vite": "5.1.4",
"vite-plugin-compression": "0.5.1",
"vite-plugin-svg-icons": "2.0.1"
},
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
}
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
autoprefixer: {},
tailwindcss: {}
}
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+15
View File
@@ -0,0 +1,15 @@
<template>
<router-view />
</template>
<script setup>
import useSettingsStore from "@/store/modules/settings";
import { handleThemeStyle } from "@/utils/theme";
onMounted(() => {
nextTick(() => {
// 初始化主题样式
handleThemeStyle(useSettingsStore().theme);
});
});
</script>
+24
View File
@@ -0,0 +1,24 @@
import request from "@/service/request";
/*新增版本号*/
export function costStandardVersionAdd(data) {
return request.post("/costCount/addCostCountTask", {}, data, {
message: {
success: "生成成功",
error: true
}
});
}
// 查询列表
export function costStandardList(params) {
return request.get(`/costCount/list`, params);
}
//删除
export function deleteCostStandard(data) {
return request.delete("/costStandard/delete", data,{
message: {
success: "删除成功",
error: true
}
});
}
+41
View File
@@ -0,0 +1,41 @@
import request from "@/service/request";
/*获取小时费率表格内容*/
export function hourRateList(params) {
return request.get(`/hourRate/list`, params);
}
/*新增小时费率*/
export function addHourRate(data) {
return request.post("/hourRate/add", {}, data, {
message: {
success: "新增成功",
error: true
}
});
}
/*编辑小时费率*/
export function editHourRate(data) {
return request.post("/hourRate/edit", {}, data, {
message: {
success: "编辑成功",
error: true
}
});
}
/*删除小时费率*/
export function deleteHourRate(data) {
return request.delete("/hourRate/delete", data,{
message: {
success: "删除成功",
error: true
}
});
}
/*批量删除小时费率*/
export function deleteBatchHourRate(data) {
return request.delete("/hourRate/deleteBatch", data, {
message: {
success: "删除成功",
error: true
}
});
}
+51
View File
@@ -0,0 +1,51 @@
import request from "@/service/request";
export function getDictCode(params) {
return request.get(`/sys/dictItem/type`, params);
}
//保存流程图
export function saveFlowModel(data) {
return request.post("/api/flowDefinition/saveModel", {}, data, {
message: {
success: `保存成功`,
error: true
}
});
}
//执行流程
export function flowInstanceExecute(id) {
return request.post(`/api/dataIntegration/flowInstance/execute/${id}`, {}, {}, {
message: {
success: `流程开始运行`,
error: true
}
});
}
//查询数据源
export function dataSourceList(params) {
return request.get("/api/group/treeList", params, {
error: true
});
}
//查询组件
export function getComponentTree(params) {
return request.get("/api/componentGroup/getComponentTree", params, {
error: true
});
}
//查询数据库列表
export function databaseListSchemas(params) {
return request.get('/api/database/listSchemas', params, {
error: true
});
}
//查询表
export function listTablesBySchemaPage(params) {
return request.get('/api/database/listTablesBySchemaPage', params);
}
// 获取表字段
export function listColumns(params) {
return request.get('/api/database/listColumns', params);
}
+40
View File
@@ -0,0 +1,40 @@
import request from "@/service/request";
/**
* @description 登录方法
*/
export function login(data) {
return request.post("/sys/login", {}, data, {
headers: {
isToken: false,
repeatSubmit: false
},
message: {
error: true
}
});
}
/**
* @description 获取用户详细信息
*/
export function getInfo() {
return request.get("/sys/user/getUserInfo");
}
/**
* @description 获取验证码
*/
export function getCodeImg(data) {
return request.get(
`/sys/randomImage/${data}`,
{},
{
headers: {
isToken: false
},
method: "get",
timeout: 20000
}
);
}
+5
View File
@@ -0,0 +1,5 @@
import request from "@/service/request";
export const listByApp = params => {
return request.get('/sys/permission/listByApp',params)
}
+44
View File
@@ -0,0 +1,44 @@
import request from "@/service/request";
/*获取模型列表*/
export function modelList(params) {
return request.get(`/cost-model/pageList`, params);
}
/*新增*/
export function addModel(data) {
return request.post("/cost-model/add", {}, data, {
message: {
success: "新增成功",
error: true
}
});
}
/*编辑*/
export function editCostModel(data) {
return request.post("/cost-model/edit", {}, data, {
message: {
success: "编辑成功",
error: true
}
});
}
/*删除*/
export function deleteModel(data) {
return request.delete("/cost-model/delete", data,{
message: {
success: "删除成功",
error: true
}
});
}
/*获取流程管理员列表*/
export function listAll() {
return request.get(`/sys/user/listAll`);
}
/*获取环境配置*/
export function environmentList(params) {
return request.get(`/api/environment/list`, params);
}
/*获取操作系统租户*/
export function tenantList(params) {
return request.get(`/api/tenant/list`, params);
}
+15
View File
@@ -0,0 +1,15 @@
import request from "@/service/request";
// 查询列表
export function costStandardDetailList(params) {
return request.get(`/costStandardDetail/list`, params);
}
/*导出excel*/
export function exportcostStandardDetail(params) {
return request.get("/costStandardDetail/exportXls", params,{ responseType:"blob"}, {
message: {
success: "导出成功",
error: true
}
});
}
+14
View File
@@ -0,0 +1,14 @@
import request from "@/service/request";
// 查询列表
export function queryBomTree(params) {
return request.get(`/bomTree/queryBomTree`, params);
}
// 查询物料编号接口
export function costMaterialBomList(params) {
return request.get(`/cost-material-bom/list`, params);
}
// 查询版本号接口
export function costStandardVersionList(params) {
return request.get(`/costStandardVersion/list`, params);
}
+50
View File
@@ -0,0 +1,50 @@
import request from "@/service/request";
/*获取小时费率表格内容*/
export function c7525Costist(params) {
return request.get(`/c7525cost/list`, params);
}
/*新增小时费率*/
export function addC7525cost(data) {
return request.post("/c7525cost/add", {}, data, {
message: {
success: "新增成功",
error: true
}
});
}
/*编辑小时费率*/
export function editC7525cost(data) {
return request.post("/c7525cost/edit", {}, data, {
message: {
success: "编辑成功",
error: true
}
});
}
/*删除小时费率*/
export function deleteC7525cost(data) {
return request.delete("/c7525cost/delete", data,{
message: {
success: "删除成功",
error: true
}
});
}
/*批量删除小时费率*/
export function deleteBatchC7525cost(data) {
return request.delete("/c7525cost/deleteBatch", data,{
message: {
success: "删除成功",
error: true
}
});
}
/*导出excel*/
export function exportExcelC7525cost(data) {
return request.get("c7525cost/exportXls", data, {
message: {
success: "导出成功",
error: true
}
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
<filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
</defs>
<g id="配置面板" width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
<g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
<g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
<mask id="mask-3" fill="white">
<use xlink:href="#path-2"></use>
</mask>
<g id="Rectangle-18">
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
<use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
</g>
<rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
<rect id="Rectangle-18" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
<filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
</defs>
<g id="配置面板" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="setting-copy-2" transform="translate(-1254.000000, -136.000000)">
<g id="Group-8" transform="translate(1167.000000, 0.000000)">
<g id="Group-5" filter="url(#filter-1)" transform="translate(89.000000, 137.000000)">
<mask id="mask-3" fill="white">
<use xlink:href="#path-2"></use>
</mask>
<g id="Rectangle-18">
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
<use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
</g>
<rect id="Rectangle-18" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
<rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

+99
View File
@@ -0,0 +1,99 @@
@import "./variables.module.scss";
@mixin colorBtn($color) {
background: $color;
&:hover {
color: $color;
&:before,
&:after {
background: $color;
}
}
}
.blue-btn {
@include colorBtn($blue);
}
.light-blue-btn {
@include colorBtn($light-blue);
}
.red-btn {
@include colorBtn($red);
}
.pink-btn {
@include colorBtn($pink);
}
.green-btn {
@include colorBtn($green);
}
.tiffany-btn {
@include colorBtn($tiffany);
}
.yellow-btn {
@include colorBtn($yellow);
}
.pan-btn {
font-size: 14px;
color: #fff;
padding: 14px 36px;
border-radius: 8px;
border: none;
outline: none;
transition: 600ms ease all;
position: relative;
display: inline-block;
&:hover {
background: #fff;
&:before,
&:after {
width: 100%;
transition: 600ms ease all;
}
}
&:before,
&:after {
content: "";
position: absolute;
top: 0;
right: 0;
height: 2px;
width: 0;
transition: 400ms ease all;
}
&::after {
right: inherit;
top: inherit;
left: 0;
bottom: 0;
}
}
.custom-button {
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
background: #fff;
color: #fff;
-webkit-appearance: none;
text-align: center;
box-sizing: border-box;
outline: 0;
margin: 0;
padding: 10px 15px;
font-size: 14px;
border-radius: 4px;
}
+103
View File
@@ -0,0 +1,103 @@
// cover some element-ui styles
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
font-weight: 400 !important;
}
.el-upload {
input[type="file"] {
display: none !important;
}
}
.el-upload__input {
display: none;
}
.cell {
.el-tag {
margin-right: 0px;
}
}
.small-padding {
.cell {
padding-left: 5px;
padding-right: 5px;
}
}
.fixed-width {
.el-button--mini {
padding: 7px 10px;
width: 60px;
}
}
.status-col {
.cell {
padding: 0 10px;
text-align: center;
.el-tag {
margin-right: 0px;
}
}
}
// to fixed https://github.com/ElemeFE/element/issues/2461
.el-dialog {
transform: none;
left: 0;
position: relative;
margin: 0 auto;
}
// refine element ui upload
.upload-container {
.el-upload {
width: 100%;
.el-upload-dragger {
width: 100%;
height: 200px;
}
}
}
// dropdown
.el-dropdown-menu {
a {
display: block;
}
}
// fix date-picker ui bug in filter-item
.el-range-editor.el-input__inner {
display: inline-flex !important;
}
// to fix el-date-picker css style
.el-range-separator {
box-sizing: content-box;
}
.el-menu--collapse
> div
> .el-submenu
> .el-submenu__title
.el-submenu__icon-arrow {
display: none;
}
.el-dropdown .el-dropdown-link {
color: var(--el-color-primary) !important;
}
.el-form-item {
.el-select,
.el-cascader,
.el-input-number {
width: 100%;
}
}
+3
View File
@@ -0,0 +1,3 @@
:root {
--el-color-primary: #2474ff;
}
+200
View File
@@ -0,0 +1,200 @@
@import "./variables.module.scss";
@import "./mixin.scss";
@import "./transition.scss";
@import "./element-ui.scss";
@import "./sidebar.scss";
@import "./btn.scss";
@import "./ruoyi.scss";
@import "element-plus/theme-chalk/src/index";
@import "@qomo-platform/core/src/index.scss";
body {
height: 100%;
margin: 0;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family:
Helvetica Neue,
Helvetica,
PingFang SC,
Hiragino Sans GB,
Microsoft YaHei,
Arial,
sans-serif;
}
label {
font-weight: 700;
}
html {
height: 100%;
box-sizing: border-box;
}
#subApp {
height: 100%;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
.no-padding {
padding: 0px !important;
}
.padding-content {
padding: 4px 0;
}
a:focus,
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
div:focus {
outline: none;
}
.fr {
float: right;
}
.fl {
float: left;
}
.pr-5 {
padding-right: 5px;
}
.pl-5 {
padding-left: 5px;
}
.block {
display: block;
}
.pointer {
cursor: pointer;
}
.inlineBlock {
display: block;
}
.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}
aside {
background: #eef1f6;
padding: 8px 24px;
margin-bottom: 20px;
border-radius: 2px;
display: block;
line-height: 32px;
font-size: 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
color: #2c3e50;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
a {
color: #337ab7;
cursor: pointer;
&:hover {
color: rgb(32, 160, 255);
}
}
}
//main-container全局样式
.app-container {
padding: 20px;
}
.components-container {
margin: 30px 50px;
position: relative;
}
.pagination-container {
margin-top: 30px;
}
.text-center {
text-align: center;
}
.sub-navbar {
height: 50px;
line-height: 50px;
position: relative;
width: 100%;
text-align: right;
padding-right: 20px;
transition: 600ms ease position;
background: linear-gradient(
90deg,
rgba(32, 182, 249, 1) 0%,
rgba(32, 182, 249, 1) 0%,
rgba(33, 120, 241, 1) 100%,
rgba(33, 120, 241, 1) 100%
);
.subtitle {
font-size: 20px;
color: #fff;
}
&.draft {
background: #d0d0d0;
}
&.deleted {
background: #d0d0d0;
}
}
.link-type,
.link-type:focus {
color: #337ab7;
cursor: pointer;
&:hover {
color: rgb(32, 160, 255);
}
}
.filter-container {
padding-bottom: 10px;
.filter-item {
display: inline-block;
vertical-align: middle;
margin-bottom: 10px;
}
}
+60
View File
@@ -0,0 +1,60 @@
@mixin clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
@mixin scrollBar {
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}
@mixin pct($pct) {
width: #{$pct};
position: relative;
margin: 0 auto;
}
@mixin triangle($width, $height, $color, $direction) {
$width: $width/2;
$color-border-style: $height solid $color;
$transparent-border-style: $width solid transparent;
height: 0;
width: 0;
@if $direction==up {
border-bottom: $color-border-style;
border-left: $transparent-border-style;
border-right: $transparent-border-style;
} @else if $direction==right {
border-left: $color-border-style;
border-top: $transparent-border-style;
border-bottom: $transparent-border-style;
} @else if $direction==down {
border-top: $color-border-style;
border-left: $transparent-border-style;
border-right: $transparent-border-style;
} @else if $direction==left {
border-right: $color-border-style;
border-top: $transparent-border-style;
border-bottom: $transparent-border-style;
}
}
+294
View File
@@ -0,0 +1,294 @@
/**
* 通用css样式布局处理
* Copyright (c) 2019 ruoyi
*/
/** 基础通用 **/
.pt5 {
padding-top: 5px;
}
.pr5 {
padding-right: 5px;
}
.pb5 {
padding-bottom: 5px;
}
.mt5 {
margin-top: 5px;
}
.mr5 {
margin-right: 5px;
}
.mb5 {
margin-bottom: 5px;
}
.mb8 {
margin-bottom: 8px;
}
.ml5 {
margin-left: 5px;
}
.mt10 {
margin-top: 10px;
}
.mr10 {
margin-right: 10px;
}
.mb10 {
margin-bottom: 10px;
}
.ml10 {
margin-left: 10px;
}
.mt20 {
margin-top: 20px;
}
.mr20 {
margin-right: 20px;
}
.mb20 {
margin-bottom: 20px;
}
.ml20 {
margin-left: 20px;
}
.h1,
.h2,
.h3,
.h4,
.h5,
.h6,
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: inherit;
font-weight: 500;
line-height: 1.1;
color: inherit;
}
.el-form .el-form-item__label {
font-weight: 700;
}
.el-dialog:not(.is-fullscreen) {
margin-top: 6vh !important;
}
.el-dialog.scrollbar .el-dialog__body {
overflow: auto;
overflow-x: hidden;
max-height: 70vh;
padding: 10px 20px 0;
}
.el-table {
.el-table__header-wrapper,
.el-table__fixed-header-wrapper {
th {
word-break: break-word;
background-color: #f8f8f9 !important;
color: #515a6e;
height: 40px !important;
font-size: 13px;
}
}
.el-table__body-wrapper {
.el-button [class*="el-icon-"] + span {
margin-left: 1px;
}
}
}
/** 表单布局 **/
.form-header {
font-size: 15px;
color: #6379bb;
border-bottom: 1px solid #ddd;
margin: 8px 10px 25px 10px;
padding-bottom: 5px;
}
/** 表格布局 **/
.pagination-container {
position: relative;
height: 25px;
margin-bottom: 10px;
margin-top: 15px;
padding: 10px 20px !important;
}
.el-dialog .pagination-container {
position: static !important;
}
/* tree border */
.tree-border {
margin-top: 5px;
border: 1px solid #e5e6e7;
background: #ffffff none;
border-radius: 4px;
width: 100%;
}
.pagination-container .el-pagination {
right: 0;
position: absolute;
}
@media (max-width: 768px) {
.pagination-container .el-pagination > .el-pagination__jump {
display: none !important;
}
.pagination-container .el-pagination > .el-pagination__sizes {
display: none !important;
}
}
.el-table .fixed-width .el-button--small {
padding-left: 0;
padding-right: 0;
width: inherit;
}
/** 表格更多操作下拉样式 */
.el-table .el-dropdown-link {
cursor: pointer;
color: #2474ff;
margin-left: 10px;
}
.el-table .el-dropdown,
.el-icon-arrow-down {
font-size: 12px;
}
.el-tree-node__content > .el-checkbox {
margin-right: 8px;
}
.list-group-striped > .list-group-item {
border-left: 0;
border-right: 0;
border-radius: 0;
padding-left: 0;
padding-right: 0;
}
.list-group {
padding-left: 0px;
list-style: none;
}
.list-group-item {
border-bottom: 1px solid #e7eaec;
border-top: 1px solid #e7eaec;
margin-bottom: -1px;
padding: 11px 0px;
font-size: 13px;
}
.pull-right {
float: right !important;
}
.el-card__header {
padding: 14px 15px 7px !important;
min-height: 40px;
}
.el-card__body {
padding: 15px 20px 20px 20px !important;
}
.card-box {
padding-right: 15px;
padding-left: 15px;
margin-bottom: 10px;
}
/* button color */
.el-button--cyan.is-active,
.el-button--cyan:active {
background: #20b2aa;
border-color: #20b2aa;
color: #ffffff;
}
.el-button--cyan:focus,
.el-button--cyan:hover {
background: #48d1cc;
border-color: #48d1cc;
color: #ffffff;
}
.el-button--cyan {
background-color: #20b2aa;
border-color: #20b2aa;
color: #ffffff;
}
/* text color */
.text-navy {
color: #1ab394;
}
.text-primary {
color: inherit;
}
.text-success {
color: #1c84c6;
}
.text-info {
color: #23c6c8;
}
.text-warning {
color: #f8ac59;
}
.text-danger {
color: #ed5565;
}
.text-muted {
color: #888888;
}
/* image */
.img-circle {
border-radius: 50%;
}
.img-lg {
width: 120px;
height: 120px;
}
.avatar-upload-preview {
position: absolute;
top: 50%;
transform: translate(50%, -50%);
width: 200px;
height: 200px;
border-radius: 50%;
box-shadow: 0 0 4px #ccc;
overflow: hidden;
}
/* 拖拽列样式 */
.sortable-ghost {
opacity: 0.8;
color: #fff !important;
background: #42b983 !important;
}
/* 表格右侧工具栏样式 */
.top-right-btn {
margin-left: auto;
}
+238
View File
@@ -0,0 +1,238 @@
#subApp {
.main-container {
height: calc(100% - 94px);
transition: margin-left 0.28s;
margin-left: $base-sidebar-width;
position: relative;
}
.sidebarHide {
margin-left: 0 !important;
}
.sidebar-container {
-webkit-transition: width 0.28s;
transition: width 0.28s;
width: $base-sidebar-width !important;
background-color: $base-menu-background;
height: calc(100% - 60px) !important;
position: fixed;
font-size: 0px;
top: 60px;
left: 0;
z-index: 1001;
overflow: hidden;
-webkit-box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
// reset element-ui css
.horizontal-collapse-transition {
transition:
0s width ease-in-out,
0s padding-left ease-in-out,
0s padding-right ease-in-out;
}
.scrollbar-wrapper {
overflow-x: hidden !important;
}
.el-scrollbar__bar.is-vertical {
right: 0px;
}
.el-scrollbar {
height: 100%;
}
&.has-logo {
.el-scrollbar {
height: calc(100% - 50px);
}
}
.is-horizontal {
display: none;
}
a {
display: inline-block;
width: 100%;
overflow: hidden;
}
.svg-icon {
margin-right: 16px;
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
}
.el-menu-item,
.menu-title {
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
.el-menu-item .el-menu-tooltip__trigger {
display: inline-block !important;
}
// menu hover
.sub-menu-title-noDropdown,
.el-sub-menu__title {
&:hover {
background-color: rgba(0, 0, 0, 0.06) !important;
}
}
& .theme-dark .is-active > .el-sub-menu__title {
color: $base-menu-color-active !important;
}
& .nest-menu .el-sub-menu > .el-sub-menu__title,
& .el-sub-menu .el-menu-item {
min-width: $base-sidebar-width !important;
&:hover {
background-color: rgba(0, 0, 0, 0.06) !important;
}
}
& .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
& .theme-dark .el-sub-menu .el-menu-item {
background-color: $base-sub-menu-background !important;
&:hover {
background-color: $base-sub-menu-hover !important;
}
}
}
.hideSidebar {
.sidebar-container {
width: 54px !important;
}
.main-container {
margin-left: 54px;
}
.sub-menu-title-noDropdown {
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
}
}
.el-sub-menu {
overflow: hidden;
& > .el-sub-menu__title {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
}
}
.el-menu--collapse {
.el-sub-menu {
& > .el-sub-menu__title {
& > span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
& > i {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
}
}
.el-menu--collapse .el-menu .el-sub-menu {
min-width: $base-sidebar-width !important;
}
// mobile responsive
.mobile {
.main-container {
margin-left: 0px;
}
.sidebar-container {
transition: transform 0.28s;
width: $base-sidebar-width !important;
}
&.hideSidebar {
.sidebar-container {
pointer-events: none;
transition-duration: 0.3s;
transform: translate3d(-$base-sidebar-width, 0, 0);
}
}
}
.withoutAnimation {
.main-container,
.sidebar-container {
transition: none;
}
}
}
// when menu collapsed
.el-menu--vertical {
& > .el-menu {
.svg-icon {
margin-right: 16px;
}
}
.nest-menu .el-sub-menu > .el-sub-menu__title,
.el-menu-item {
&:hover {
// you can use $sub-menuHover
background-color: rgba(0, 0, 0, 0.06) !important;
}
}
// the scroll bar appears when the sub-menu is too long
> .el-menu--popup {
max-height: 100vh;
overflow-y: auto;
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
}
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+49
View File
@@ -0,0 +1,49 @@
// global transition css
/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.28s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* fade-transform */
.fade-transform--move,
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 0.5s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* breadcrumb transition */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all 0.5s;
}
.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all 0.5s;
}
.breadcrumb-leave-active {
position: absolute;
}
+65
View File
@@ -0,0 +1,65 @@
// base color
$blue: #324157;
$light-blue: #3a71a8;
$red: #c03639;
$pink: #e65d6e;
$green: #30b08f;
$tiffany: #4ab7bd;
$yellow: #fec171;
$panGreen: #30b08f;
// 默认菜单主题风格
$base-menu-color: #bfcbd9;
$base-menu-color-active: #f4f4f5;
$base-menu-background: #304156;
$base-logo-title-color: #ffffff;
$base-menu-light-color: rgba(0, 0, 0, 0.7);
$base-menu-light-background: #ffffff;
$base-logo-light-title-color: #001529;
$base-sub-menu-background: #1f2d3d;
$base-sub-menu-hover: #001528;
// 自定义暗色菜单风格
/**
$base-menu-color:hsla(0,0%,100%,.65);
$base-menu-color-active:#fff;
$base-menu-background:#001529;
$base-logo-title-color: #ffffff;
$base-menu-light-color:rgba(0,0,0,.70);
$base-menu-light-background:#ffffff;
$base-logo-light-title-color: #001529;
$base-sub-menu-background:#000c17;
$base-sub-menu-hover:#001528;
*/
$--color-primary: #2474ff;
$--color-success: #67c23a;
$--color-warning: #e6a23c;
$--color-danger: #f56c6c;
$--color-info: #909399;
$base-sidebar-width: 200px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
menuColor: $base-menu-color;
menuLightColor: $base-menu-light-color;
menuColorActive: $base-menu-color-active;
menuBackground: $base-menu-background;
menuLightBackground: $base-menu-light-background;
subMenuBackground: $base-sub-menu-background;
subMenuHover: $base-sub-menu-hover;
sideBarWidth: $base-sidebar-width;
logoTitleColor: $base-logo-title-color;
logoLightTitleColor: $base-logo-light-title-color;
primaryColor: $--color-primary;
successColor: $--color-success;
dangerColor: $--color-danger;
infoColor: $--color-info;
warningColor: $--color-warning;
}
+76
View File
@@ -0,0 +1,76 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span
v-if="
item.redirect === 'noRedirect' || index === levelList.length - 1
"
class="no-redirect"
>{{ item.meta.title }}</span
>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script setup>
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const levelList = ref([]);
function getBreadcrumb() {
// only show routes with meta.title
let matched = route.matched.filter(item => item.meta && item.meta.title);
const first = matched[0];
// 判断是否为首页
if (!isDashboard(first)) {
matched = [{ path: "/index", meta: { title: "首页" } }].concat(matched);
}
levelList.value = matched.filter(
item => item.meta && item.meta.title && item.meta.breadcrumb !== false
);
}
function isDashboard(route) {
const name = route && route.name;
if (!name) {
return false;
}
return name.trim() === "Index";
}
function handleLink(item) {
const { redirect, path } = item;
if (redirect) {
router.push(redirect);
return;
}
router.push(path);
}
watchEffect(() => {
// if you go to the redirect page, do not update the breadcrumbs
if (route.path.startsWith("/redirect/")) {
return;
}
getBreadcrumb();
});
getBreadcrumb();
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>
+36
View File
@@ -0,0 +1,36 @@
<script lang="tsx">
//表格
import { defineComponent, ref, onMounted, watch } from "vue";
import * as echarts from 'echarts';
export default defineComponent({
props: {
option:Object
},
setup(props) {
const chartRef = ref();
const render = () => {
return (
<div ref={chartRef} style="width: 100%; height: 100%;">
</div>
);
};
onMounted(() => {
// 初始化图表
const chartInstance = echarts.init(chartRef.value);
chartInstance.setOption(props.option);
window.addEventListener('resize', () => {
chartInstance.resize();
});
});
watch(props, (newVal, oldVal) => {
if (newVal.option) {
const chartInstance = echarts.init(chartRef.value);
chartInstance.setOption(props.option);
}
});
return render;
},
});
</script>
<style lang="scss" scoped></style>
+3
View File
@@ -0,0 +1,3 @@
<template>
<router-view />
</template>
+245
View File
@@ -0,0 +1,245 @@
<script lang="tsx">
import {
computed,
defineComponent,
ref,
onMounted,
watchEffect,
watch
} from "vue";
import { useExposeDialogArgs } from "@qomo-platform/core";
import { useRequest } from "vue-request";
import {
dataSourceList,
databaseListSchemas,
listTablesBySchemaPage,
listColumns
} from "@/api";
import icon_report from "@/assets/imgs/icon_report.png";
import icon_folder from "@/assets/imgs/icon_folder.png";
import dateIcon from "@/assets/imgs/dateIcon.png";
import numberIcon from "@/assets/imgs/numberIcon.png";
import stringIcon from "@/assets/imgs/stringIcon.png";
import databaseIcon from "@/assets/imgs/database.png";
import { ElMessage } from "element-plus";
import { da } from "element-plus/es/locale";
export default defineComponent({
name: "SqlStatement",
props: {
model: {
type: Object
},
},
setup(props, { attrs, expose, slots = {} }) {
const typeNumber = [
"int",
"smallint",
"bigint",
"float",
"double",
"decimal",
"bit"
],
typeDate = ["date", "datetime", "time", "timestamp"],
typeString = ["char", "varchar", "varchar2", "text"];
const { data, error } = useRequest(async () => await dataSourceList());
const {
run,
data: _schemaData_,
loading
} = useRequest(databaseListSchemas, {
manual: true
});
const {
run: listTablesBySchemaPageRun,
data: tablesData,
loading: listTablesLoading
} = useRequest(listTablesBySchemaPage, {
manual: true
});
const {
run: listColumnsRun,
data: listColumnsData,
loading: listColumnsLoading
} = useRequest(listColumns, {
manual: true
});
const schemaData = computed(() =>
(_schemaData_.value || []).map(item => ({
label: item.name,
value: item.name
}))
);
const handleNodeClick = async node => {
if (node.nodeType) {
props.model.dataSource = node.name;
props.model.url = node.url;
props.model.username = node.userName;
props.model.password = node.password;
props.model.currentNode = node;
run({ id: node.id });
} else ElMessage.warning("选中的不是数据源,请重新选择");
};
watch(
() => props.model.schema,
val => {
if (val)
listTablesBySchemaPageRun({
id: props.model.currentNode.id,
schemaName: props.model.schema,
tableName: "",
typeList: "table,view",
pageNo: 1,
pageSize: 1000
});
}
);
const filterNodeMethod = (value, data) => data.name.includes(value);
const loadNode = async (node, resolve) => {
const { data } = node;
if (node?.level !== 0) {
await listColumnsRun({
id: props.model.currentNode.id,
tableName: data.name,
schemaName: props.model.schema
});
resolve(listColumnsData.value);
}
};
const filterSearch = async value => {
listTablesBySchemaPageRun({
id: props.model.currentNode.id,
schemaName: props.model.schema,
tableName: value,
typeList: "table,view",
pageNo: 1,
pageSize: 1000
});
};
const handleTableClick = node => {
props.model.sqlString = `select * from ${node.name}`;
// this.$refs.codemirrorTic.$refs.mycode.__vue__.focus();
};
return () => (
<>
<base-form-item label="数据源:" prop="dataSource" span={8}>
<el-tree-select
v-model={props.model.dataSource}
data={data.value}
value-key="name"
filter-node-method={filterNodeMethod}
filterable
render-after-expand={false}
style="width: 240px"
v-slots={{
default: ({ data }) => (
<div
class="flex items-center gap-[5px]"
onClick={() => handleNodeClick(data)}
>
{data.children && !data.parentId && (
<img class="w-[15px] h-[15px]" src={icon_report} alt="" />
)}
{data.children && data.parentId && (
<img class="w-[15px] h-[15px]" src={icon_folder} alt="" />
)}
<span class={"text-[12px] text-[#26314a] font-[400]"}>
{data.name}
</span>
</div>
)
}}
/>
</base-form-item>
<base-form-item label="schema:" prop="schema" span={8}>
<base-select
disabled={loading.value}
placeholder={loading.value ? "加载中" : "请选择"}
v-model={props.model.schema}
options={schemaData.value}
></base-select>
</base-form-item>
<div
class={
"max-h-[400px] overflow-auto border-solid border-[1px] border-[#ccc] p-[10px]"
}
>
<base-tree
data={tablesData.value?.records}
filterables={{
size: "small"
}}
props={{
label: "name"
}}
load={loadNode}
filterSearch={filterSearch}
lazy
v-slots={{
default: ({ node, data }) => {
return (
<div
onClick={() => handleTableClick(data)}
class={"flex items-center gap-[4px] text-[12px]"}
>
{node.level === 1 && (
<img
class="w-[15px] h-[15px]"
src={databaseIcon}
alt=""
/>
)}
{!data.columns &&
typeNumber.includes(
(data?.type || "").toLocaleLowerCase()
) && (
<img
class="w-[15px] h-[15px]"
src={numberIcon}
alt=""
/>
)}
{!data.columns &&
typeDate.includes(
(data?.type || "").toLocaleLowerCase()
) && (
<img class="w-[15px] h-[15px]" src={dateIcon} alt="" />
)}
{!data.columns &&
typeString.includes(
(data?.type || "").toLocaleLowerCase()
) && (
<img
class="w-[15px] h-[15px]"
src={stringIcon}
alt=""
/>
)}
<span class="text-[12px]">
{data.name}
{data.comment && <span>({data.comment})</span>}
</span>
</div>
);
}
}}
/>
</div>
</>
);
}
});
</script>
<style scoped lang="scss"></style>
+273
View File
@@ -0,0 +1,273 @@
<script lang="tsx">
import {
computed,
defineComponent,
ref,
onMounted,
watchEffect,
watch
} from "vue";
import { useExposeDialogArgs } from "@qomo-platform/core";
import { useRequest } from "vue-request";
import {
dataSourceList,
databaseListSchemas,
listTablesBySchemaPage,
listColumns
} from "@/api";
import icon_report from "@/assets/imgs/icon_report.png";
import icon_folder from "@/assets/imgs/icon_folder.png";
import dateIcon from "@/assets/imgs/dateIcon.png";
import numberIcon from "@/assets/imgs/numberIcon.png";
import stringIcon from "@/assets/imgs/stringIcon.png";
import databaseIcon from "@/assets/imgs/database.png";
import { ElMessage } from "element-plus";
import { da } from "element-plus/es/locale";
export default defineComponent({
name: "SqlStatement",
props: {
model: {
type: Object
}
},
setup(props, { attrs, expose, slots = {} }) {
const typeNumber = [
"int",
"smallint",
"bigint",
"float",
"double",
"decimal",
"bit"
],
typeDate = ["date", "datetime", "time", "timestamp"],
typeString = ["char", "varchar", "varchar2", "text"];
const { data, error } = useRequest(async () => await dataSourceList());
const {
run,
data: _schemaData_,
loading
} = useRequest(databaseListSchemas, {
manual: true
});
const {
run: listTablesBySchemaPageRun,
data: tablesData,
loading: listTablesLoading
} = useRequest(listTablesBySchemaPage, {
manual: true
});
const {
run: listColumnsRun,
data: listColumnsData,
loading: listColumnsLoading
} = useRequest(listColumns, {
manual: true
});
const schemaData = computed(() =>
(_schemaData_.value || []).map(item => ({
label: item.name,
value: item.name
}))
);
const handleNodeClick = async node => {
if (node.nodeType) {
props.model.dataSource = node.name;
props.model.url = node.url;
props.model.username = node.userName;
props.model.password = node.password;
props.model.currentNode = node;
run({ id: node.id });
} else ElMessage.warning("选中的不是数据源,请重新选择");
};
watch(
() => props.model.schema,
val => {
if (val)
listTablesBySchemaPageRun({
id: props.model.currentNode.id,
schemaName: props.model.schema,
tableName: "",
typeList: "table,view",
pageNo: 1,
pageSize: 1000
});
}
);
const filterNodeMethod = (value, data) => data.name.includes(value);
const loadNode = async (node, resolve) => {
const { data } = node;
if (node?.level !== 0) {
await listColumnsRun({
id: props.model.currentNode.id,
tableName: data.name,
schemaName: props.model.schema
});
resolve(listColumnsData.value);
}
};
const filterSearch = async value => {
listTablesBySchemaPageRun({
id: props.model.currentNode.id,
schemaName: props.model.schema,
tableName: value,
typeList: "table,view",
pageNo: 1,
pageSize: 1000
});
};
const handleTableClick = node => {
props.model.sqlString = `select * from ${node.name}`;
// this.$refs.codemirrorTic.$refs.mycode.__vue__.focus();
};
const feeTypeList = [
{
label: "机器折旧费",
value: "EquipmentFee"
},
{
label: "辅料费",
value: "SupplyMaterialFee"
},
{
label: "水电费",
value: "DriverFee"
},
{
label: "其他费用",
value: "OtherFee"
},
{
label: "物流费",
value: "LogisticsFee"
}
];
return () => (
<>
<base-form-item label="费用类型:" prop="feeType" span={8}>
<base-select
placeholder={loading.value ? "加载中" : "请选择"}
v-model={props.model.feeType}
options={feeTypeList}
></base-select>
</base-form-item>
<base-form-item label="数据源:" prop="dataSource" span={8}>
<el-tree-select
v-model={props.model.dataSource}
data={data.value}
value-key="name"
filter-node-method={filterNodeMethod}
filterable
render-after-expand={false}
style="width: 240px"
v-slots={{
default: ({ data }) => (
<div
class="flex items-center gap-[5px]"
onClick={() => handleNodeClick(data)}
>
{data.children && !data.parentId && (
<img class="w-[15px] h-[15px]" src={icon_report} alt="" />
)}
{data.children && data.parentId && (
<img class="w-[15px] h-[15px]" src={icon_folder} alt="" />
)}
<span class={"text-[12px] text-[#26314a] font-[400]"}>
{data.name}
</span>
</div>
)
}}
/>
</base-form-item>
<base-form-item label="schema:" prop="schema" span={8}>
<base-select
disabled={loading.value}
placeholder={loading.value ? "加载中" : "请选择"}
v-model={props.model.schema}
options={schemaData.value}
></base-select>
</base-form-item>
<div
class={
"max-h-[400px] overflow-auto border-solid border-[1px] border-[#ccc] p-[10px]"
}
>
<base-tree
data={tablesData.value?.records}
filterables={{
size: "small"
}}
props={{
label: "name"
}}
load={loadNode}
filterSearch={filterSearch}
lazy
v-slots={{
default: ({ node, data }) => {
return (
<div
onClick={() => handleTableClick(data)}
class={"flex items-center gap-[4px] text-[12px]"}
>
{node.level === 1 && (
<img
class="w-[15px] h-[15px]"
src={databaseIcon}
alt=""
/>
)}
{!data.columns &&
typeNumber.includes(
(data?.type || "").toLocaleLowerCase()
) && (
<img
class="w-[15px] h-[15px]"
src={numberIcon}
alt=""
/>
)}
{!data.columns &&
typeDate.includes(
(data?.type || "").toLocaleLowerCase()
) && (
<img class="w-[15px] h-[15px]" src={dateIcon} alt="" />
)}
{!data.columns &&
typeString.includes(
(data?.type || "").toLocaleLowerCase()
) && (
<img
class="w-[15px] h-[15px]"
src={stringIcon}
alt=""
/>
)}
<span class="text-[12px]">
{data.name}
{data.comment && <span>({data.comment})</span>}
</span>
</div>
);
}
}}
/>
</div>
</>
);
}
});
</script>
<style scoped lang="scss"></style>
+32
View File
@@ -0,0 +1,32 @@
<template>
<div v-loading="loading" :style="'height:' + height">
<iframe
:src="url"
frameborder="no"
style="width: 100%; height: 100%"
scrolling="auto"
/>
</div>
</template>
<script setup>
const props = defineProps({
src: {
type: String,
required: true
}
});
const height = ref(document.documentElement.clientHeight - 94.5 + "px;");
const loading = ref(true);
const url = computed(() => props.src);
onMounted(() => {
setTimeout(() => {
loading.value = false;
}, 300);
window.onresize = function temp() {
height.value = document.documentElement.clientHeight - 94.5 + "px;";
};
});
</script>
+66
View File
@@ -0,0 +1,66 @@
/**
* v-copyText 复制文本内容
* Copyright (c) 2022 ruoyi
*/
export default {
beforeMount(el, { value, arg }) {
if (arg === "callback") {
el.$copyCallback = value;
} else {
el.$copyValue = value;
const handler = () => {
copyTextToClipboard(el.$copyValue);
if (el.$copyCallback) {
el.$copyCallback(el.$copyValue);
}
};
el.addEventListener("click", handler);
el.$destroyCopy = () => el.removeEventListener("click", handler);
}
}
};
function copyTextToClipboard(input, { target = document.body } = {}) {
const element = document.createElement("textarea");
const previouslyFocusedElement = document.activeElement;
element.value = input;
// Prevent keyboard from showing on mobile
element.setAttribute("readonly", "");
element.style.contain = "strict";
element.style.position = "absolute";
element.style.left = "-9999px";
element.style.fontSize = "12pt"; // Prevent zooming on iOS
const selection = document.getSelection();
const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0);
target.append(element);
element.select();
// Explicit selection workaround for iOS
element.selectionStart = 0;
element.selectionEnd = input.length;
let isSuccess = false;
try {
isSuccess = document.execCommand("copy");
} catch {}
element.remove();
if (originalRange) {
selection.removeAllRanges();
selection.addRange(originalRange);
}
// Get the focus back on the previously focused element, if any
if (previouslyFocusedElement) {
previouslyFocusedElement.focus();
}
return isSuccess;
}
+9
View File
@@ -0,0 +1,9 @@
import hasRole from "./permission/hasRole";
import hasPermi from "./permission/hasPermi";
import copyText from "./common/copyText";
export default function directive(app) {
app.directive("hasRole", hasRole);
app.directive("hasPermi", hasPermi);
app.directive("copyText", copyText);
}
+29
View File
@@ -0,0 +1,29 @@
/**
* v-hasPermi 操作权限处理
*/
import useUserStore from "@/store/modules/user";
export default {
mounted(el, binding) {
const { value } = binding;
const all_permission = "*:*:*";
const permissions = useUserStore().permissions;
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value;
const hasPermissions = permissions.some(permission => {
return (
all_permission === permission || permissionFlag.includes(permission)
);
});
if (!hasPermissions) {
el.parentNode && el.parentNode.removeChild(el);
}
} else {
throw new Error(`请设置操作权限标签值`);
}
}
};
+28
View File
@@ -0,0 +1,28 @@
/**
* v-hasRole 角色权限处理
* Copyright (c) 2019 ruoyi
*/
import useUserStore from "@/store/modules/user";
export default {
mounted(el, binding, vnode) {
const { value } = binding;
const super_admin = "admin";
const roles = useUserStore().roles;
if (value && value instanceof Array && value.length > 0) {
const roleFlag = value;
const hasRole = roles.some(role => {
return super_admin === role || roleFlag.includes(role);
});
if (!hasRole) {
el.parentNode && el.parentNode.removeChild(el);
}
} else {
throw new Error(`请设置角色权限标签值`);
}
}
};
@@ -0,0 +1,102 @@
<script lang="tsx">
import {
computed,
defineComponent,
ref,
onMounted,
watchEffect,
watch
} from "vue";
import { useExposeDialogArgs } from "@qomo-platform/core";
import SqlStatement from "@/components/SqlStatement";
export default defineComponent({
name: "dbOutput",
props: {
nodeDetail: {
type: Object
}
},
setup(props, { attrs, expose, slots = {} }) {
const nodeDetail = computed(() => props?.nodeDetail);
const baseFormRef = ref({});
const baseMonacoEditorRef = ref();
const { findNodesPrev, findNodesNext, getNodeData } = attrs;
const submit = () => {
let params = baseFormRef.value.formData;
return {
params
};
};
const open = () => {
init(nodeDetail.value);
baseMonacoEditorRef.value?.restLayout();
};
// onMounted(()=>{
// init(nodeDetail.value)
// })
// watch(nodeDetail,(v)=>{
// init(v)
// },{
// deep:true
// })
watch(
() => baseFormRef.value.formData,
val => {},
{
deep: true
}
);
const init = v => {
const data = findNodesNext(props?.nodeDetail.id);
baseFormRef.value.setFormData(
JSON.parse(v?.taskParams || "{}")?.params || {}
);
};
const handleOK = value => {};
expose({
submit,
open
});
return () => (
<base-form
ref={baseFormRef}
v-slots={{
default: ({ model }) => (
<div class={"flex gap-[10px] w-full"}>
<div class={"w-[300px] overflow-auto flex flex-col"}>
<SqlStatement
baseFormRef={baseFormRef}
model={model}
></SqlStatement>
</div>
<div class={"flex flex-1 flex-col"}>
<base-form-item label="SQL语句:" prop="test" span={8}>
<div class={"w-full h-[200px]"}>
<BaseMonacoEditor
ref={baseMonacoEditorRef}
v-model={model.sqlString}
language="sql"
customReminders={{
test: ["liaoqiuyu"]
}}
></BaseMonacoEditor>
</div>
</base-form-item>
</div>
</div>
)
}}
/>
);
}
});
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,102 @@
<script lang="tsx">
import {
computed,
defineComponent,
ref,
onMounted,
watchEffect,
watch
} from "vue";
import { useExposeDialogArgs } from "@qomo-platform/core";
import SqlStatementType from "@/components/SqlStatementType";
export default defineComponent({
name: "dbOutput",
props: {
nodeDetail: {
type: Object
}
},
setup(props, { attrs, expose, slots = {} }) {
const nodeDetail = computed(() => props?.nodeDetail);
const baseFormRef = ref({});
const baseMonacoEditorRef = ref();
const { findNodesPrev, findNodesNext, getNodeData } = attrs;
const submit = () => {
let params = baseFormRef.value.formData;
return {
params
};
};
const open = () => {
init(nodeDetail.value);
baseMonacoEditorRef.value?.restLayout();
};
// onMounted(()=>{
// init(nodeDetail.value)
// })
// watch(nodeDetail,(v)=>{
// init(v)
// },{
// deep:true
// })
watch(
() => baseFormRef.value.formData,
val => {},
{
deep: true
}
);
const init = v => {
const data = findNodesNext(props?.nodeDetail.id);
baseFormRef.value.setFormData(
JSON.parse(v?.taskParams || "{}")?.params || {}
);
};
const handleOK = value => {};
expose({
submit,
open
});
return () => (
<base-form
ref={baseFormRef}
v-slots={{
default: ({ model }) => (
<div class={"flex gap-[10px] w-full"}>
<div class={"w-[300px] overflow-auto flex flex-col"}>
<SqlStatementType
baseFormRef={baseFormRef}
model={model}
></SqlStatementType>
</div>
<div class={"flex flex-1 flex-col"}>
<base-form-item label="SQL语句:" prop="test" span={8}>
<div class={"w-full h-[200px]"}>
<BaseMonacoEditor
ref={baseMonacoEditorRef}
v-model={model.sqlString}
language="sql"
customReminders={{
test: ["liaoqiuyu"]
}}
></BaseMonacoEditor>
</div>
</base-form-item>
</div>
</div>
)
}}
/>
);
}
});
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,102 @@
<script lang="tsx">
import {
computed,
defineComponent,
ref,
onMounted,
watchEffect,
watch
} from "vue";
import { useExposeDialogArgs } from "@qomo-platform/core";
import SqlStatement from "@/components/SqlStatement";
export default defineComponent({
name: "dbOutput",
props: {
nodeDetail: {
type: Object
}
},
setup(props, { attrs, expose, slots = {} }) {
const nodeDetail = computed(() => props?.nodeDetail);
const baseFormRef = ref({});
const baseMonacoEditorRef = ref();
const { findNodesPrev, findNodesNext, getNodeData } = attrs;
const submit = () => {
let params = baseFormRef.value.formData;
return {
params
};
};
const open = () => {
init(nodeDetail.value);
baseMonacoEditorRef.value?.restLayout();
};
// onMounted(()=>{
// init(nodeDetail.value)
// })
// watch(nodeDetail,(v)=>{
// init(v)
// },{
// deep:true
// })
watch(
() => baseFormRef.value.formData,
val => {},
{
deep: true
}
);
const init = v => {
const data = findNodesNext(props?.nodeDetail.id);
baseFormRef.value.setFormData(
JSON.parse(v?.taskParams || "{}")?.params || {}
);
};
const handleOK = value => {};
expose({
submit,
open
});
return () => (
<base-form
ref={baseFormRef}
v-slots={{
default: ({ model }) => (
<div class={"flex gap-[10px] w-full"}>
<div class={"w-[300px] overflow-auto flex flex-col"}>
<SqlStatement
baseFormRef={baseFormRef}
model={model}
></SqlStatement>
</div>
<div class={"flex flex-1 flex-col"}>
<base-form-item label="SQL语句:" prop="test" span={8}>
<div class={"w-full h-[200px]"}>
<BaseMonacoEditor
ref={baseMonacoEditorRef}
v-model={model.sqlString}
language="sql"
customReminders={{
test: ["liaoqiuyu"]
}}
></BaseMonacoEditor>
</div>
</base-form-item>
</div>
</div>
)
}}
/>
);
}
});
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,102 @@
<script lang="tsx">
import {
computed,
defineComponent,
ref,
onMounted,
watchEffect,
watch
} from "vue";
import { useExposeDialogArgs } from "@qomo-platform/core";
import SqlStatement from "@/components/SqlStatement";
export default defineComponent({
name: "dbOutput",
props: {
nodeDetail: {
type: Object
}
},
setup(props, { attrs, expose, slots = {} }) {
const nodeDetail = computed(() => props?.nodeDetail);
const baseFormRef = ref({});
const baseMonacoEditorRef = ref();
const { findNodesPrev, findNodesNext, getNodeData } = attrs;
const submit = () => {
let params = baseFormRef.value.formData;
return {
params
};
};
const open = () => {
init(nodeDetail.value);
baseMonacoEditorRef.value?.restLayout();
};
// onMounted(()=>{
// init(nodeDetail.value)
// })
// watch(nodeDetail,(v)=>{
// init(v)
// },{
// deep:true
// })
watch(
() => baseFormRef.value.formData,
val => {},
{
deep: true
}
);
const init = v => {
const data = findNodesNext(props?.nodeDetail.id);
baseFormRef.value.setFormData(
JSON.parse(v?.taskParams || "{}")?.params || {}
);
};
const handleOK = value => {};
expose({
submit,
open
});
return () => (
<base-form
ref={baseFormRef}
v-slots={{
default: ({ model }) => (
<div class={"flex gap-[10px] w-full"}>
<div class={"w-[300px] overflow-auto flex flex-col"}>
<SqlStatement
baseFormRef={baseFormRef}
model={model}
></SqlStatement>
</div>
<div class={"flex flex-1 flex-col"}>
<base-form-item label="SQL语句:" prop="test" span={8}>
<div class={"w-full h-[200px]"}>
<BaseMonacoEditor
ref={baseMonacoEditorRef}
v-model={model.sqlString}
language="sql"
customReminders={{
test: ["liaoqiuyu"]
}}
></BaseMonacoEditor>
</div>
</base-form-item>
</div>
</div>
)
}}
/>
);
}
});
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,102 @@
<script lang="tsx">
import {
computed,
defineComponent,
ref,
onMounted,
watchEffect,
watch
} from "vue";
import { useExposeDialogArgs } from "@qomo-platform/core";
import SqlStatement from "@/components/SqlStatement";
export default defineComponent({
name: "dbOutput",
props: {
nodeDetail: {
type: Object
}
},
setup(props, { attrs, expose, slots = {} }) {
const nodeDetail = computed(() => props?.nodeDetail);
const baseFormRef = ref({});
const baseMonacoEditorRef = ref();
const { findNodesPrev, findNodesNext, getNodeData } = attrs;
const submit = () => {
let params = baseFormRef.value.formData;
return {
params
};
};
const open = () => {
init(nodeDetail.value);
baseMonacoEditorRef.value?.restLayout();
};
// onMounted(()=>{
// init(nodeDetail.value)
// })
// watch(nodeDetail,(v)=>{
// init(v)
// },{
// deep:true
// })
watch(
() => baseFormRef.value.formData,
val => {},
{
deep: true
}
);
const init = v => {
const data = findNodesNext(props?.nodeDetail.id);
baseFormRef.value.setFormData(
JSON.parse(v?.taskParams || "{}")?.params || {}
);
};
const handleOK = value => {};
expose({
submit,
open
});
return () => (
<base-form
ref={baseFormRef}
v-slots={{
default: ({ model }) => (
<div class={"flex gap-[10px] w-full"}>
<div class={"w-[300px] overflow-auto flex flex-col"}>
<SqlStatement
baseFormRef={baseFormRef}
model={model}
></SqlStatement>
</div>
<div class={"flex flex-1 flex-col"}>
<base-form-item label="SQL语句:" prop="test" span={8}>
<div class={"w-full h-[200px]"}>
<BaseMonacoEditor
ref={baseMonacoEditorRef}
v-model={model.sqlString}
language="sql"
customReminders={{
test: ["liaoqiuyu"]
}}
></BaseMonacoEditor>
</div>
</base-form-item>
</div>
</div>
)
}}
/>
);
}
});
</script>
<style scoped lang="scss"></style>
+102
View File
@@ -0,0 +1,102 @@
<script lang="tsx">
import {
computed,
defineComponent,
ref,
onMounted,
watchEffect,
watch
} from "vue";
import { useExposeDialogArgs } from "@qomo-platform/core";
import SqlStatement from "@/components/SqlStatement";
export default defineComponent({
name: "dbOutput",
props: {
nodeDetail: {
type: Object
}
},
setup(props, { attrs, expose, slots = {} }) {
const nodeDetail = computed(() => props?.nodeDetail);
const baseFormRef = ref({});
const baseMonacoEditorRef = ref();
const { findNodesPrev, findNodesNext, getNodeData } = attrs;
const submit = () => {
let params = baseFormRef.value.formData;
return {
params
};
};
const open = () => {
init(nodeDetail.value);
baseMonacoEditorRef.value?.restLayout();
};
// onMounted(()=>{
// init(nodeDetail.value)
// })
// watch(nodeDetail,(v)=>{
// init(v)
// },{
// deep:true
// })
watch(
() => baseFormRef.value.formData,
val => {},
{
deep: true
}
);
const init = v => {
const data = findNodesNext(props?.nodeDetail.id);
baseFormRef.value.setFormData(
JSON.parse(v?.taskParams || "{}")?.params || {}
);
};
const handleOK = value => {};
expose({
submit,
open
});
return () => (
<base-form
ref={baseFormRef}
v-slots={{
default: ({ model }) => (
<div class={"flex gap-[10px] w-full"}>
<div class={"w-[300px] overflow-auto flex flex-col"}>
<SqlStatement
baseFormRef={baseFormRef}
model={model}
></SqlStatement>
</div>
<div class={"flex flex-1 flex-col"}>
<base-form-item label="SQL语句:" prop="test" span={8}>
<div class={"w-full h-[200px]"}>
<BaseMonacoEditor
ref={baseMonacoEditorRef}
v-model={model.code}
language="sql"
customReminders={{
test: ["liaoqiuyu"]
}}
></BaseMonacoEditor>
</div>
</base-form-item>
</div>
</div>
)
}}
/>
);
}
});
</script>
<style scoped lang="scss"></style>
+20
View File
@@ -0,0 +1,20 @@
import { App, defineAsyncComponent } from "vue";
import { getComponentNameByPath } from "@qomo-platform/core";
//组件
const components = Object.assign(
{},
import.meta.glob("/src/flowableComponents/**/*.(vue|tsx|jsx)")
);
const FlowableComponents = {
install(app: App) {
Object.entries(components).map(([componentPath, component]) =>
app.component(
getComponentNameByPath(componentPath),
defineAsyncComponent(component)
)
);
}
};
export default FlowableComponents;
+71
View File
@@ -0,0 +1,71 @@
<template>
<section class="app-main">
<router-view v-slot="{ Component, route }">
<transition mode="out-in" name="fade-transform">
<keep-alive :include="tagsViewStore.cachedViews">
<component
:is="Component"
v-if="!route.meta.link"
:key="route.path"
/>
</keep-alive>
</transition>
</router-view>
<iframe-toggle />
</section>
</template>
<script setup>
import iframeToggle from "./IframeToggle/index";
import useTagsViewStore from "@/store/modules/tagsView";
const tagsViewStore = useTagsViewStore();
</script>
<style lang="scss" scoped>
.app-main {
max-height: 100%;
width: 100%;
position: relative;
overflow: auto;
background: #f5f7fa;
padding: 8px;
}
.fixed-header + .app-main {
padding-top: 50px;
}
.hasTagsView {
.app-main {
/* 84 = navbar + tags-view = 50 + 34 */
}
.fixed-header + .app-main {
padding-top: 84px;
}
}
</style>
<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
.fixed-header {
padding-right: 6px;
}
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background-color: #c0c0c0;
border-radius: 3px;
}
</style>
@@ -0,0 +1,27 @@
<template>
<inner-link
v-for="(item, index) in tagsViewStore.iframeViews"
:key="item.path"
:iframeId="'iframe' + index"
v-show="route.path === item.path"
:src="iframeUrl(item.meta.link, item.query)"
></inner-link>
</template>
<script setup>
import InnerLink from "../InnerLink/index";
import useTagsViewStore from "@/store/modules/tagsView";
const route = useRoute();
const tagsViewStore = useTagsViewStore();
function iframeUrl(url, query) {
if (Object.keys(query).length > 0) {
let params = Object.keys(query)
.map(key => key + "=" + query[key])
.join("&");
return url + "?" + params;
}
return url;
}
</script>
+24
View File
@@ -0,0 +1,24 @@
<template>
<div :style="'height:' + height">
<iframe
:id="iframeId"
style="width: 100%; height: 100%"
:src="src"
frameborder="no"
></iframe>
</div>
</template>
<script setup>
const props = defineProps({
src: {
type: String,
default: "/"
},
iframeId: {
type: String
}
});
const height = ref(document.documentElement.clientHeight - 94.5 + "px");
</script>
+152
View File
@@ -0,0 +1,152 @@
<template>
<!-- -->
<div
:style="{ background: settingsStore.theme }"
class="navbar h-[60px] flex justify-between"
>
<div class="left-box flex items-center">
<div class="logo-box h-full flex items-center pl-4 cursor-pointer">
<div class="logo w-[180px] h-[40px]"></div>
<div class="home w-[14px] h-[14px]"></div>
</div>
<hamburger
id="hamburger-container"
:is-active="appStore.sidebar.opened"
class="hamburger-container"
@toggleClick="toggleSideBar"
/>
<!-- <breadcrumb id="breadcrumb-container" class="breadcrumb-container" />-->
</div>
</div>
</template>
<script setup>
import { ElMessageBox } from "element-plus";
import Breadcrumb from "@/components/Breadcrumb";
import useAppStore from "@/store/modules/app";
import useSettingsStore from "@/store/modules/settings";
import useUserStore from "@/store/modules/user";
const appStore = useAppStore();
const userStore = useUserStore();
const settingsStore = useSettingsStore();
defineProps({
showBreadcrumb: { type: Boolean, default: true } // 是否显示左侧面包屑
});
function toggleSideBar() {
appStore.toggleSideBar();
}
function handleCommand(command) {
switch (command) {
case "setLayout":
setLayout();
break;
default:
break;
}
}
const emits = defineEmits(["setLayout"]);
function setLayout() {
emits("setLayout");
}
</script>
<style lang="scss" scoped>
.navbar {
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.logo {
background: url("@/assets/images/logo.png") no-repeat;
background-size: cover;
}
.home {
background: url("@/assets/images/home.png") no-repeat;
background-size: cover;
}
.hamburger-container {
line-height: 60px;
height: 100%;
float: left;
cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
color: #fff;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
.breadcrumb-container {
float: left;
color: #fff !important;
}
.topmenu-container {
position: absolute;
left: 50px;
}
.errLog-container {
display: inline-block;
vertical-align: top;
}
.right-menu {
float: right;
height: 100%;
display: flex;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #fff;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background 0.3s;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
}
.avatar-container {
margin-right: 40px;
.avatar-wrapper {
margin-top: 10px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 50%;
}
i {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
}
</style>
+264
View File
@@ -0,0 +1,264 @@
<template>
<el-drawer
v-model="showSettings"
:withHeader="false"
direction="rtl"
size="300px"
>
<div class="setting-drawer-title">
<h3 class="drawer-title">主题风格设置</h3>
</div>
<div class="setting-drawer-block-checbox">
<div
class="setting-drawer-block-checbox-item"
@click="handleTheme('theme-dark')"
>
<img alt="dark" src="@/assets/images/dark.svg" />
<div
v-if="sideTheme === 'theme-dark'"
class="setting-drawer-block-checbox-selectIcon"
style="display: block"
>
<i aria-label="图标: check" class="anticon anticon-check">
<svg
:fill="theme"
aria-hidden="true"
class
data-icon="check"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</i>
</div>
</div>
<div
class="setting-drawer-block-checbox-item"
@click="handleTheme('theme-light')"
>
<img alt="light" src="@/assets/images/light.svg" />
<div
v-if="sideTheme === 'theme-light'"
class="setting-drawer-block-checbox-selectIcon"
style="display: block"
>
<i aria-label="图标: check" class="anticon anticon-check">
<svg
:fill="theme"
aria-hidden="true"
class
data-icon="check"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</i>
</div>
</div>
</div>
<div class="drawer-item">
<span>主题颜色</span>
<span class="comp-style">
<el-color-picker
v-model="theme"
:predefine="predefineColors"
@change="themeChange"
/>
</span>
</div>
<el-divider />
<h3 class="drawer-title">系统布局配置</h3>
<div class="drawer-item">
<span>开启 TopNav</span>
<span class="comp-style">
<el-switch
v-model="settingsStore.topNav"
class="drawer-switch"
@change="topNavChange"
/>
</span>
</div>
<div class="drawer-item">
<span>开启 Tags-Views</span>
<span class="comp-style">
<el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>固定 Header</span>
<span class="comp-style">
<el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>显示 Logo</span>
<span class="comp-style">
<el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>动态标题</span>
<span class="comp-style">
<el-switch v-model="settingsStore.dynamicTitle" class="drawer-switch" />
</span>
</div>
<el-divider />
<el-button icon="DocumentAdd" plain type="primary" @click="saveSetting"
>保存配置</el-button
>
<el-button icon="Refresh" plain @click="resetSetting">重置配置</el-button>
</el-drawer>
</template>
<script setup>
import variables from "@/assets/styles/variables.module.scss";
import axios from "axios";
import { ElLoading, ElMessage } from "element-plus";
import { useDynamicTitle } from "@/utils/dynamicTitle";
import useAppStore from "@/store/modules/app";
import useSettingsStore from "@/store/modules/settings";
import usePermissionStore from "@/store/modules/permission";
import { handleThemeStyle } from "@/utils/theme";
const { proxy } = getCurrentInstance();
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const showSettings = ref(false);
const theme = ref(settingsStore.theme);
const sideTheme = ref(settingsStore.sideTheme);
const storeSettings = computed(() => settingsStore);
const predefineColors = ref([
"#2474ff",
"#ff4500",
"#ff8c00",
"#ffd700",
"#90ee90",
"#00ced1",
"#1e90ff",
"#c71585"
]);
/** 是否需要topnav */
function topNavChange(val) {
if (!val) {
appStore.toggleSideBarHide(false);
permissionStore.setSidebarRouters(permissionStore.defaultRoutes);
}
}
function themeChange(val) {
settingsStore.theme = val;
handleThemeStyle(val);
}
function handleTheme(val) {
settingsStore.sideTheme = val;
sideTheme.value = val;
}
function saveSetting() {
proxy.$modal.loading("正在保存到本地,请稍候...");
let layoutSetting = {
topNav: storeSettings.value.topNav,
tagsView: storeSettings.value.tagsView,
fixedHeader: storeSettings.value.fixedHeader,
sidebarLogo: storeSettings.value.sidebarLogo,
dynamicTitle: storeSettings.value.dynamicTitle,
sideTheme: storeSettings.value.sideTheme,
theme: storeSettings.value.theme
};
localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
setTimeout(proxy.$modal.closeLoading(), 1000);
}
function resetSetting() {
proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...");
localStorage.removeItem("layout-setting");
setTimeout("window.location.reload()", 1000);
}
function openSetting() {
showSettings.value = true;
}
defineExpose({
openSetting
});
</script>
<style lang="scss" scoped>
.setting-drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, 0.85);
line-height: 22px;
font-weight: bold;
.drawer-title {
font-size: 14px;
}
}
.setting-drawer-block-checbox {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 10px;
margin-bottom: 20px;
.setting-drawer-block-checbox-item {
position: relative;
margin-right: 16px;
border-radius: 2px;
cursor: pointer;
img {
width: 48px;
height: 48px;
}
.custom-img {
width: 48px;
height: 38px;
border-radius: 5px;
box-shadow: 1px 1px 2px #898484;
}
.setting-drawer-block-checbox-selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
padding-top: 15px;
padding-left: 24px;
color: #1890ff;
font-weight: 700;
font-size: 14px;
}
}
}
.drawer-item {
color: rgba(0, 0, 0, 0.65);
padding: 12px 0;
font-size: 14px;
.comp-style {
float: right;
margin: -3px 8px 0px 0px;
}
}
</style>
+40
View File
@@ -0,0 +1,40 @@
<template>
<component :is="type" v-bind="linkProps()">
<slot />
</component>
</template>
<script setup>
import { isExternal } from "@/utils/validate";
const props = defineProps({
to: {
type: [String, Object],
required: true
}
});
const isExt = computed(() => {
return isExternal(props.to);
});
const type = computed(() => {
if (isExt.value) {
return "a";
}
return "router-link";
});
function linkProps() {
if (isExt.value) {
return {
href: props.to,
target: "_blank",
rel: "noopener"
};
}
return {
to: props.to
};
}
</script>
+124
View File
@@ -0,0 +1,124 @@
<template>
<div
:class="{ collapse: collapse }"
:style="{
backgroundColor:
sideTheme === 'theme-dark'
? variables.menuBackground
: variables.menuLightBackground
}"
class="sidebar-logo-container"
>
<transition name="sidebarLogoFade">
<router-link
v-if="collapse"
key="collapse"
class="sidebar-logo-link"
to="/"
>
<div v-if="logo" class="sidebar-logo"></div>
<!-- <img v-if="logo" :src="logo" alt="" class="sidebar-logo" />-->
<h1
v-else
:style="{
color:
sideTheme === 'theme-dark'
? variables.logoTitleColor
: variables.logoLightTitleColor
}"
class="sidebar-title"
>
{{ title }}
</h1>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1
:style="{
color:
sideTheme === 'theme-dark'
? variables.logoTitleColor
: variables.logoLightTitleColor
}"
class="sidebar-title"
>
{{ title }}
</h1>
</router-link>
</transition>
</div>
</template>
<script setup>
import variables from "@/assets/styles/variables.module.scss";
import logo from "@/assets/logo/logo.png";
import useSettingsStore from "@/store/modules/settings";
defineProps({
collapse: {
type: Boolean,
required: true
}
});
const title = import.meta.env.VITE_APP_TITLE;
const settingsStore = useSettingsStore();
const sideTheme = computed(() => settingsStore.sideTheme);
</script>
<style lang="scss" scoped>
.sidebarLogoFade-enter-active {
transition: opacity 1.5s;
}
.sidebarLogoFade-enter,
.sidebarLogoFade-leave-to {
opacity: 0;
}
.sidebar-logo-container {
position: relative;
width: 100%;
height: 50px;
line-height: 50px;
background: #2b2f3a;
text-align: center;
overflow: hidden;
background: url("@/assets/images/logo.png") no-repeat;
background-size: cover;
& .sidebar-logo-link {
height: 100%;
width: 100%;
& .sidebar-logo {
width: 32px;
height: 32px;
vertical-align: middle;
margin-right: 12px;
}
& .sidebar-title {
display: inline-block;
margin: 0;
color: #fff;
font-weight: 600;
line-height: 50px;
font-size: 14px;
font-family:
Avenir,
Helvetica Neue,
Arial,
Helvetica,
sans-serif;
vertical-align: middle;
}
}
&.collapse {
.sidebar-logo {
margin-right: 0px;
}
}
}
</style>
@@ -0,0 +1,134 @@
<template>
<div v-if="!item.hidden">
<template
v-if="
hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
!item.alwaysShow
"
>
<app-link
v-if="onlyOneChild.meta"
:to="resolvePath(onlyOneChild.path, onlyOneChild.query)"
>
<el-menu-item
:index="resolvePath(onlyOneChild.path)"
:class="{ 'submenu-title-noDropdown': !isNest }"
>
<base-svg
:icon-class="
onlyOneChild.meta.icon || (item.meta && item.meta.icon)
"
/>
<template #title
><span
class="menu-title"
:title="hasTitle(onlyOneChild.meta.title)"
>{{ onlyOneChild.meta.title }}</span
></template
>
</el-menu-item>
</app-link>
</template>
<el-sub-menu
v-else
ref="subMenu"
:index="resolvePath(item.path)"
teleported
>
<template v-if="item.meta" #title>
<base-svg :icon-class="item.meta && item.meta.icon" />
<span class="menu-title" :title="hasTitle(item.meta.title)">{{
item.meta.title
}}</span>
</template>
<sidebar-item
v-for="(child, index) in item.children"
:key="child.path + index"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-sub-menu>
</div>
</template>
<script setup>
import { isExternal } from "@/utils/validate";
import AppLink from "./Link";
import { getNormalPath } from "@/utils/ruoyi";
const props = defineProps({
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ""
}
});
const onlyOneChild = ref({});
function hasOneShowingChild(children = [], parent) {
if (!children) {
children = [];
}
const showingChildren = children.filter(item => {
if (item.hidden) {
return false;
} else {
// Temp set(will be used if only has one showing child)
onlyOneChild.value = item;
return true;
}
});
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true;
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
return true;
}
return false;
}
function resolvePath(routePath, routeQuery) {
if (isExternal(routePath)) {
return routePath;
}
if (isExternal(props.basePath)) {
return props.basePath;
}
if (routeQuery) {
let query = JSON.parse(routeQuery);
return {
path: getNormalPath(props.basePath + "/" + routePath),
query: query
};
}
return getNormalPath(props.basePath + "/" + routePath);
}
function hasTitle(title) {
if (title.length > 5) {
return title;
} else {
return "";
}
}
</script>
+68
View File
@@ -0,0 +1,68 @@
<template>
<div
:class="{ 'has-logo': showLogo }"
:style="{
backgroundColor:
sideTheme === 'theme-dark'
? variables.menuBackground
: variables.menuLightBackground
}"
>
<el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
<el-menu
:active-text-color="theme"
:background-color="
sideTheme === 'theme-dark'
? variables.menuBackground
: variables.menuLightBackground
"
:collapse="isCollapse"
:collapse-transition="false"
:default-active="activeMenu"
:text-color="
sideTheme === 'theme-dark'
? variables.menuColor
: variables.menuLightColor
"
:unique-opened="true"
mode="vertical"
>
<sidebar-item
v-for="(route, index) in sidebarRouters"
:key="route.path + index"
:base-path="route.path"
:item="route"
/>
</el-menu>
</el-scrollbar>
</div>
</template>
<script setup>
import Logo from "./Logo";
import SidebarItem from "./SidebarItem";
import variables from "@/assets/styles/variables.module.scss";
import useAppStore from "@/store/modules/app";
import useSettingsStore from "@/store/modules/settings";
import usePermissionStore from "@/store/modules/permission";
import { useRoute } from "vue-router";
const route = useRoute();
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const sidebarRouters = computed(() => permissionStore.sidebarRouters);
const showLogo = computed(() => settingsStore.sidebarLogo);
const sideTheme = computed(() => settingsStore.sideTheme);
const theme = computed(() => settingsStore.theme);
const isCollapse = computed(() => !appStore.sidebar.opened);
const activeMenu = computed(() => {
const { meta, path } = route;
if (meta.activeMenu) {
return meta.activeMenu;
}
return path;
});
</script>
@@ -0,0 +1,114 @@
<template>
<el-scrollbar
ref="scrollContainer"
:vertical="false"
class="scroll-container"
@wheel.prevent="handleScroll"
>
<slot />
</el-scrollbar>
</template>
<script setup>
import useTagsViewStore from "@/store/modules/tagsView";
const tagAndTagSpacing = ref(4);
const { proxy } = getCurrentInstance();
const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef);
onMounted(() => {
scrollWrapper.value.addEventListener("scroll", emitScroll, true);
});
onBeforeUnmount(() => {
scrollWrapper.value.removeEventListener("scroll", emitScroll);
});
function handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40;
const $scrollWrapper = scrollWrapper.value;
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4;
}
const emits = defineEmits();
const emitScroll = () => {
emits("scroll");
};
const tagsViewStore = useTagsViewStore();
const visitedViews = computed(() => tagsViewStore.visitedViews);
function moveToTarget(currentTag) {
const $container = proxy.$refs.scrollContainer.$el;
const $containerWidth = $container.offsetWidth;
const $scrollWrapper = scrollWrapper.value;
let firstTag = null;
let lastTag = null;
// find first tag and last tag
if (visitedViews.value.length > 0) {
firstTag = visitedViews.value[0];
lastTag = visitedViews.value[visitedViews.value.length - 1];
}
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0;
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
} else {
const tagListDom = document.getElementsByClassName("tags-view-item");
const currentIndex = visitedViews.value.findIndex(
item => item === currentTag
);
let prevTag = null;
let nextTag = null;
for (const k in tagListDom) {
if (k !== "length" && Object.hasOwnProperty.call(tagListDom, k)) {
if (
tagListDom[k].dataset.path ===
visitedViews.value[currentIndex - 1].path
) {
prevTag = tagListDom[k];
}
if (
tagListDom[k].dataset.path ===
visitedViews.value[currentIndex + 1].path
) {
nextTag = tagListDom[k];
}
}
}
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft =
nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing.value;
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value;
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
}
}
}
defineExpose({
moveToTarget
});
</script>
<style lang="scss" scoped>
.scroll-container {
white-space: nowrap;
position: relative;
overflow: hidden;
width: 100%;
:deep(.el-scrollbar__bar) {
bottom: 0px;
}
:deep(.el-scrollbar__wrap) {
height: 39px;
}
}
</style>
+357
View File
@@ -0,0 +1,357 @@
<template>
<div id="tags-view-container" class="tags-view-container">
<scroll-pane
ref="scrollPaneRef"
class="tags-view-wrapper"
@scroll="handleScroll"
>
<router-link
v-for="tag in visitedViews"
:key="tag.path"
:data-path="tag.path"
:class="isActive(tag) ? 'active' : ''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
class="tags-view-item"
:style="activeStyle(tag)"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openMenu(tag, $event)"
>
{{ tag.title }}
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
<close
class="el-icon-close"
style="width: 1em; height: 1em; vertical-align: middle"
/>
</span>
</router-link>
</scroll-pane>
<ul
v-show="visible"
:style="{ left: left + 'px', top: top + 'px' }"
class="contextmenu"
>
<li @click="refreshSelectedTag(selectedTag)">
<refresh-right style="width: 1em; height: 1em" /> 刷新页面
</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<close style="width: 1em; height: 1em" /> 关闭当前
</li>
<li @click="closeOthersTags">
<circle-close style="width: 1em; height: 1em" /> 关闭其他
</li>
<li v-if="!isFirstView()" @click="closeLeftTags">
<back style="width: 1em; height: 1em" /> 关闭左侧
</li>
<li v-if="!isLastView()" @click="closeRightTags">
<right style="width: 1em; height: 1em" /> 关闭右侧
</li>
<li @click="closeAllTags(selectedTag)">
<circle-close style="width: 1em; height: 1em" /> 全部关闭
</li>
</ul>
</div>
</template>
<script setup>
import ScrollPane from "./ScrollPane";
import { getNormalPath } from "@/utils/ruoyi";
import useTagsViewStore from "@/store/modules/tagsView";
import useSettingsStore from "@/store/modules/settings";
import usePermissionStore from "@/store/modules/permission";
const visible = ref(false);
const top = ref(0);
const left = ref(0);
const selectedTag = ref({});
const affixTags = ref([]);
const scrollPaneRef = ref(null);
const { proxy } = getCurrentInstance();
const route = useRoute();
const router = useRouter();
const visitedViews = computed(() => useTagsViewStore().visitedViews);
const routes = computed(() => usePermissionStore().routes);
const theme = computed(() => useSettingsStore().theme);
watch(route, () => {
addTags();
moveToCurrentTag();
});
watch(visible, value => {
if (value) {
document.body.addEventListener("click", closeMenu);
} else {
document.body.removeEventListener("click", closeMenu);
}
});
onMounted(() => {
initTags();
addTags();
});
function isActive(r) {
return r.path === route.path;
}
function activeStyle(tag) {
if (!isActive(tag)) return {};
return {
"background-color": theme.value,
"border-color": theme.value
};
}
function isAffix(tag) {
return tag.meta && tag.meta.affix;
}
function isFirstView() {
try {
return (
selectedTag.value.fullPath === "/index" ||
selectedTag.value.fullPath === visitedViews.value[1].fullPath
);
} catch (err) {
return false;
}
}
function isLastView() {
try {
return (
selectedTag.value.fullPath ===
visitedViews.value[visitedViews.value.length - 1].fullPath
);
} catch (err) {
return false;
}
}
function filterAffixTags(routes, basePath = "") {
let tags = [];
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = getNormalPath(basePath + "/" + route.path);
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta }
});
}
if (route.children) {
const tempTags = filterAffixTags(route.children, route.path);
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags];
}
}
});
return tags;
}
function initTags() {
const res = filterAffixTags(routes.value);
affixTags.value = res;
for (const tag of res) {
// Must have tag name
if (tag.name) {
useTagsViewStore().addVisitedView(tag);
}
}
}
function addTags() {
const { name } = route;
if (name) {
useTagsViewStore().addView(route);
if (route.meta.link) {
useTagsViewStore().addIframeView(route);
}
}
return false;
}
function moveToCurrentTag() {
nextTick(() => {
for (const r of visitedViews.value) {
if (r.path === route.path) {
scrollPaneRef.value.moveToTarget(r);
// when query is different then update
if (r.fullPath !== route.fullPath) {
useTagsViewStore().updateVisitedView(route);
}
}
}
});
}
function refreshSelectedTag(view) {
proxy.$tab.refreshPage(view);
if (route.meta.link) {
useTagsViewStore().delIframeView(route);
}
}
function closeSelectedTag(view) {
proxy.$tab.closePage(view).then(({ visitedViews }) => {
if (isActive(view)) {
toLastView(visitedViews, view);
}
});
}
function closeRightTags() {
proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
toLastView(visitedViews);
}
});
}
function closeLeftTags() {
proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
toLastView(visitedViews);
}
});
}
function closeOthersTags() {
router.push(selectedTag.value).catch(() => {});
proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
moveToCurrentTag();
});
}
function closeAllTags(view) {
proxy.$tab.closeAllPage().then(({ visitedViews }) => {
if (affixTags.value.some(tag => tag.path === route.path)) {
return;
}
toLastView(visitedViews, view);
});
}
function toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0];
if (latestView) {
router.push(latestView.fullPath);
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === "Dashboard") {
// to reload home page
router.replace({ path: "/redirect" + view.fullPath });
} else {
router.push("/");
}
}
}
function openMenu(tag, e) {
const menuMinWidth = 105;
const offsetLeft = proxy.$el.getBoundingClientRect().left; // container margin left
const offsetWidth = proxy.$el.offsetWidth; // container width
const maxLeft = offsetWidth - menuMinWidth; // left boundary
const l = e.clientX - offsetLeft + 15; // 15: margin right
if (l > maxLeft) {
left.value = maxLeft;
} else {
left.value = l;
}
top.value = e.clientY;
visible.value = true;
selectedTag.value = tag;
}
function closeMenu() {
visible.value = false;
}
function handleScroll() {
closeMenu();
}
</script>
<style lang="scss" scoped>
.tags-view-container {
height: 34px;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow:
0 1px 3px 0 rgba(0, 0, 0, 0.12),
0 0 3px 0 rgba(0, 0, 0, 0.04);
.tags-view-wrapper {
.tags-view-item {
display: inline-block;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: #42b983;
color: #fff;
border-color: #42b983;
&::before {
content: "";
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 5px;
}
}
}
}
.contextmenu {
margin: 0;
background: #fff;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
}
</style>
<style lang="scss">
//reset element css of el-icon-close
.tags-view-wrapper {
.tags-view-item {
.el-icon-close {
width: 16px;
height: 16px;
vertical-align: 2px;
border-radius: 50%;
text-align: center;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(0.6);
display: inline-block;
vertical-align: -3px;
}
&:hover {
background-color: #b4bccc;
color: #fff;
width: 12px !important;
height: 12px !important;
}
}
}
}
</style>
+4
View File
@@ -0,0 +1,4 @@
export { default as AppMain } from "./AppMain";
export { default as Navbar } from "./Navbar";
export { default as Settings } from "./Settings";
export { default as TagsView } from "./TagsView/index.vue";
+135
View File
@@ -0,0 +1,135 @@
<template>
<div
:class="classObj"
:style="{ '--current-color': theme }"
class="app-wrapper"
v-if="!isQIANKUN"
>
<div
v-if="device === 'mobile' && sidebar.opened"
class="drawer-bg"
@click="handleClickOutside"
/>
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
<div :class="{ 'fixed-header': fixedHeader }">
<navbar @setLayout="setLayout" />
<div class="tags-view-container-box">
<tags-view v-if="needTagsView" />
</div>
</div>
<div
:class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }"
class="main-container"
>
<app-main />
<settings ref="settingRef" />
</div>
</div>
<app-main v-else />
</template>
<script setup>
import { useWindowSize } from "@vueuse/core";
import Sidebar from "./components/Sidebar/index.vue";
import { AppMain, Navbar, Settings, TagsView } from "./components";
import useAppStore from "@/store/modules/app";
import useSettingsStore from "@/store/modules/settings";
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";
const settingsStore = useSettingsStore();
const theme = computed(() => settingsStore.theme);
const sideTheme = computed(() => settingsStore.sideTheme);
const sidebar = computed(() => useAppStore().sidebar);
const device = computed(() => useAppStore().device);
const needTagsView = computed(() => settingsStore.tagsView);
const fixedHeader = computed(() => settingsStore.fixedHeader);
const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened,
openSidebar: sidebar.value.opened,
withoutAnimation: sidebar.value.withoutAnimation,
mobile: device.value === "mobile"
}));
const { width, height } = useWindowSize();
const WIDTH = 992; // refer to Bootstrap's responsive design
const isQIANKUN = computed(() => qiankunWindow.__POWERED_BY_QIANKUN__);
watchEffect(() => {
if (device.value === "mobile" && sidebar.value.opened) {
useAppStore().closeSideBar({ withoutAnimation: false });
}
if (width.value - 1 < WIDTH) {
useAppStore().toggleDevice("mobile");
useAppStore().closeSideBar({ withoutAnimation: true });
} else {
useAppStore().toggleDevice("desktop");
}
});
function handleClickOutside() {
useAppStore().closeSideBar({ withoutAnimation: false });
}
const settingRef = ref(null);
function setLayout() {
settingRef.value.openSetting();
}
</script>
<!-- <style lang="scss" scoped>
@import "@/assets/styles/mixin.scss";
@import "@/assets/styles/variables.module.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px);
}
.hideSidebar .tags-view-container-box {
width: calc(100% - 54px);
margin-left: 54px;
}
.tags-view-container-box {
width: calc(100% - 200px);
margin-left: #{$base-sidebar-width};
transition: margin-left 0.28s;
}
.sidebarHide .fixed-header {
width: 100%;
}
.mobile .fixed-header {
width: 100%;
}
</style> -->
+87
View File
@@ -0,0 +1,87 @@
import { createApp } from "vue";
import { isHttp } from "@/utils/validate";
import "./assets/styles/tailwind.css";
import Cookies from "js-cookie";
import "@/utils/flexible.js";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import locale from "element-plus/es/locale/lang/zh-cn";
import "@/assets/styles/index.scss";
import App from "./App";
import store from "./store";
import router from "./router";
import directive from "./directive"; // directive
import { qomoPlatformOptions } from "./platform";
import QomoPlatform from "@qomo-platform/core";
// 注册指令
import plugins from "./plugins"; // plugins
import {
renderWithQiankun,
qiankunWindow
} from "vite-plugin-qiankun/dist/helper";
// svg图标
import "virtual:svg-icons-register";
import usePermissionStore from "@/store/modules/permission";
import "@/assets/styles/global.css";
import FlowableComponents from "@/flowableComponents/index.ts"
let app;
function render(props = {}) {
const { container } = props;
app = createApp(App);
app.use(store);
usePermissionStore()
.generateRoutes()
.then(routes => {
routes.forEach(route => {
if (!isHttp(route.path)) {
router.addRoute(route); // 动态添加可访问路由表
}
});
app.use(router);
app.use(plugins);
app.use(QomoPlatform, qomoPlatformOptions);
app.use(FlowableComponents);
directive(app);
// 使用element-plus 并且设置全局的大小
app.use(ElementPlus, {
name:'testApp',
locale: locale,
// 支持 large、default、small
size: Cookies.get("size") || "default"
});
app.mount(container ? container.querySelector("#subApp") : "#subApp");
});
}
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
render();
} else {
renderWithQiankun({
mount(props) {
console.log("===mount===");
render(props);
},
bootstrap() {
console.log("===bootstrap===");
},
unmount(props) {
console.log(app);
console.log("===unmount===");
const { container } = props;
app.unmount();
},
update(props) {
console.log("===update===", props);
}
});
}
+259
View File
@@ -0,0 +1,259 @@
<template>
<div class="login">
<el-form
ref="loginRef"
:model="loginForm"
:rules="loginRules"
class="login-form"
>
<h3 class="title">Qomo Ai</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
auto-complete="off"
placeholder="账号"
size="large"
type="text"
>
<template #prefix
><base-svg class="el-input__icon input-icon" icon-class="user"
/></template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
auto-complete="off"
placeholder="密码"
size="large"
type="password"
@keyup.enter="handleLogin"
>
<template #prefix
><base-svg class="el-input__icon input-icon" icon-class="password"
/></template>
</el-input>
</el-form-item>
<el-form-item v-if="captchaEnabled" prop="code">
<el-input
v-model="loginForm.captcha"
auto-complete="off"
placeholder="验证码"
size="large"
style="width: 63%"
@keyup.enter="handleLogin"
>
<template #prefix
><base-svg class="el-input__icon input-icon" icon-class="validCode"
/></template>
</el-input>
<div class="login-code">
<img :src="codeUrl" class="login-code-img" @click="getCode" />
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0"
>记住密码</el-checkbox
>
<el-form-item style="width: 100%">
<el-button
:loading="loading"
size="large"
style="width: 100%"
type="primary"
@click.prevent="handleLogin"
>
<span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span>
</el-button>
<div v-if="register" style="float: right">
<router-link :to="'/register'" class="link-type"
>立即注册</router-link
>
</div>
</el-form-item>
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2018-2023 ruoyi.vip All Rights Reserved.</span>
</div>
</div>
</template>
<script setup>
import { getCodeImg } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from "@/utils/jsencrypt";
import useUserStore from "@/store/modules/user";
import { useRoute, useRouter } from "vue-router";
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance();
const loginForm = ref({
username: "admin",
password: "123456",
rememberMe: false,
captcha: ""
});
const loginRules = {
username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],
captcha: [{ required: true, trigger: "change", message: "请输入验证码" }]
};
const codeUrl = ref("");
const loading = ref(false);
// 验证码开关
const captchaEnabled = ref(true);
// 注册开关
const register = ref(false);
const redirect = ref(undefined);
const currDateTime = ref();
const loginRef = ref(null);
watch(
route,
newRoute => {
console.log(newRoute.query.redirect, "newRoute.query.redirect");
console.log(redirect.value, " redirect.value");
redirect.value = newRoute.query && newRoute.query.redirect;
},
{ immediate: true }
);
function handleLogin() {
loginRef.value.validate(valid => {
if (valid) {
loading.value = true;
// 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码
if (loginForm.value.rememberMe) {
Cookies.set("username", loginForm.value.username, { expires: 30 });
Cookies.set("password", encrypt(loginForm.value.password), {
expires: 30
});
Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 });
} else {
// 否则移除
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove("rememberMe");
}
// 调用action的登录方法
userStore
.login({ ...loginForm.value, checkKey: currDateTime.value })
.then(() => {
const query = route.query;
const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
if (cur !== "redirect") {
acc[cur] = query[cur];
}
return acc;
}, {});
router.push({
path: redirect.value || "/main",
query: otherQueryParams
});
})
.catch(() => {
loading.value = false;
// 重新获取验证码
if (captchaEnabled.value) {
getCode();
}
});
}
});
}
/**
* @description 获取验证码
*/
async function getCode() {
currDateTime.value = new Date().getTime();
const result = await getCodeImg(currDateTime.value);
codeUrl.value = result;
}
function getCookie() {
const username = Cookies.get("username");
const password = Cookies.get("password");
const rememberMe = Cookies.get("rememberMe");
loginForm.value = {
username: username === undefined ? loginForm.value.username : username,
password:
password === undefined ? loginForm.value.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
};
}
getCode();
getCookie();
</script>
<style lang="scss" scoped>
.login {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-image: url("@/assets/images/login-background.jpg");
background-size: cover;
}
.title {
margin: 0px auto 30px auto;
text-align: center;
color: #707070;
}
.login-form {
border-radius: 6px;
background: #ffffff;
width: 400px;
padding: 25px 25px 5px 25px;
.el-input {
height: 40px;
input {
height: 40px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 0px;
}
}
.login-tip {
font-size: 13px;
text-align: center;
color: #bfbfbf;
}
.login-code {
width: 33%;
height: 40px;
float: right;
img {
cursor: pointer;
vertical-align: middle;
}
}
.el-login-footer {
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
font-size: 12px;
letter-spacing: 1px;
}
.login-code-img {
height: 40px;
padding-left: 12px;
}
</style>
+47
View File
@@ -0,0 +1,47 @@
<script setup>
const leftMenu = ref([
{
title: "首页",
icon: "home",
path: "/index"
},
{
title: "系统管理",
icon: "setting",
path: "/system"
}
]);
</script>
<template>
<div
class="left-menu-box w-[120px] bg-white h-full flex flex-col items-center"
>
<template v-for="item in leftMenu" :key="item.path">
<div
class="menu-box w-[100px] flex flex-col items-center p-[11px] rounded cursor-pointer transition-all duration-300 ease-in-out mt-[8px]"
>
<base-svg
:icon-class="item.icon"
class-name="text-[#A1A7B2] text-[22px] icon-box"
/>
<span class="text-[#A1A7B2] text-[14px] inline-block mt-2">{{
item.title
}}</span>
</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.menu-box {
&:hover {
background-color: var(--el-color-primary);
color: white;
span,
.icon-box {
color: white;
}
}
}
</style>
+125
View File
@@ -0,0 +1,125 @@
<template>
<div
:class="classObj"
:style="{ '--current-color': theme }"
class="app-wrapper"
>
<div
v-if="device === 'mobile' && sidebar.opened"
class="drawer-bg"
@click="handleClickOutside"
/>
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }">
<div :class="{ 'fixed-header': fixedHeader }">
<navbar :show-breadcrumb="false" @setLayout="setLayout" />
</div>
<div class="main-box">
<left-menu></left-menu>
</div>
<settings ref="settingRef" />
</div>
</div>
</template>
<script setup>
import { useWindowSize } from "@vueuse/core";
import { AppMain, Navbar, Settings, TagsView } from "@/layout/components";
import LeftMenu from "./components/LeftMenu.vue";
import useAppStore from "@/store/modules/app";
import useSettingsStore from "@/store/modules/settings";
const settingsStore = useSettingsStore();
const theme = computed(() => settingsStore.theme);
const sideTheme = computed(() => settingsStore.sideTheme);
const sidebar = computed(() => useAppStore().sidebar);
const device = computed(() => useAppStore().device);
const needTagsView = computed(() => settingsStore.tagsView);
const fixedHeader = computed(() => settingsStore.fixedHeader);
const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened,
openSidebar: sidebar.value.opened,
withoutAnimation: sidebar.value.withoutAnimation,
mobile: device.value === "mobile"
}));
const { width, height } = useWindowSize();
const WIDTH = 992; // refer to Bootstrap's responsive design
watchEffect(() => {
if (device.value === "mobile" && sidebar.value.opened) {
useAppStore().closeSideBar({ withoutAnimation: false });
}
if (width.value - 1 < WIDTH) {
useAppStore().toggleDevice("mobile");
useAppStore().closeSideBar({ withoutAnimation: true });
} else {
useAppStore().toggleDevice("desktop");
}
});
function handleClickOutside() {
useAppStore().closeSideBar({ withoutAnimation: false });
}
const settingRef = ref(null);
function setLayout() {
settingRef.value.openSetting();
}
</script>
<style lang="scss" scoped>
@import "@/assets/styles/mixin.scss";
@import "@/assets/styles/variables.module.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.main-box {
height: calc(100vh - 60px);
background: #f5f7fa;
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px);
}
.hideSidebar .fixed-header {
width: calc(100% - 54px);
}
.sidebarHide .fixed-header {
width: 100%;
}
.mobile .fixed-header {
width: 100%;
}
</style>
+83
View File
@@ -0,0 +1,83 @@
<template>
<div class="errPage-container">
<el-button icon="arrow-left" class="pan-back-btn" @click="back">
返回
</el-button>
<el-row>
<el-col :span="12">
<h1 class="text-jumbo text-ginormous">401错误!</h1>
<h2>您没有访问权限!</h2>
<h6>对不起,您没有访问权限,请不要进行非法操作!您可以返回主页面</h6>
<ul class="list-unstyled">
<li class="link-type">
<router-link to="/"> 回首页 </router-link>
</li>
</ul>
</el-col>
<el-col :span="12">
<img
:src="errGif"
width="313"
height="428"
alt="Girl has dropped her ice cream."
/>
</el-col>
</el-row>
</div>
</template>
<script setup>
import errImage from "@/assets/401_images/401.gif";
let { proxy } = getCurrentInstance();
const errGif = ref(errImage + "?" + +new Date());
function back() {
if (proxy.$route.query.noGoBack) {
proxy.$router.push({ path: "/" });
} else {
proxy.$router.go(-1);
}
}
</script>
<style lang="scss" scoped>
.errPage-container {
width: 800px;
max-width: 100%;
margin: 100px auto;
.pan-back-btn {
background: #008489;
color: #fff;
border: none !important;
}
.pan-gif {
margin: 0 auto;
display: block;
}
.pan-img {
display: block;
margin: 0 auto;
width: 100%;
}
.text-jumbo {
font-size: 60px;
font-weight: 700;
color: #484848;
}
.list-unstyled {
font-size: 14px;
li {
padding-bottom: 5px;
}
a {
color: #008489;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
</style>
+241
View File
@@ -0,0 +1,241 @@
<template>
<div class="wscn-http404-container">
<div class="wscn-http404">
<div class="pic-404">
<img
class="pic-404__parent"
src="../../assets/404_images/404.png"
alt="404"
/>
<img
class="pic-404__child left"
src="../../assets/404_images/404_cloud.png"
alt="404"
/>
<img
class="pic-404__child mid"
src="../../assets/404_images/404_cloud.png"
alt="404"
/>
<img
class="pic-404__child right"
src="../../assets/404_images/404_cloud.png"
alt="404"
/>
</div>
<div class="bullshit">
<div class="bullshit__oops">404错误!</div>
<div class="bullshit__headline">
{{ message }}
</div>
<div class="bullshit__info">
对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
</div>
<router-link to="/index" class="bullshit__return-home">
返回首页
</router-link>
</div>
</div>
</div>
</template>
<script setup>
let message = computed(() => {
return "找不到网页!";
});
</script>
<style lang="scss" scoped>
.wscn-http404-container {
transform: translate(-50%, -50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
}
}
</style>
+14
View File
@@ -0,0 +1,14 @@
<template>
<div></div>
</template>
<script setup>
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const { params, query } = route;
const { path } = params;
router.replace({ path: "/" + path, query });
</script>
+61
View File
@@ -0,0 +1,61 @@
import router from "./router";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import { getToken } from "@/utils/auth";
import { isHttp } from "@/utils/validate";
import { isRelogin } from "@/service/request";
import useUserStore from "@/store/modules/user";
import useSettingsStore from "@/store/modules/settings";
import usePermissionStore from "@/store/modules/permission";
NProgress.configure({ showSpinner: false });
// const whiteList = ["/login", "/register"];
// router.beforeEach((to, from, next) => {
// NProgress.start();
// if (getToken()) {
// to.meta.title && useSettingsStore().setTitle(to.meta.title);
// /* has token*/
// if (to.path === "/login") {
// next({ path: "/" });
// NProgress.done();
// } else if (whiteList.indexOf(to.path) !== -1) {
// next();
// } else {
// if (useUserStore().roles.length === 0) {
// isRelogin.show = true;
// useUserStore()
// .getInfo()
// .then(() => {
// isRelogin.show = true;
// usePermissionStore()
// .generateRoutes()
// .then(routes => {
// routes.forEach(route => {
// if (!isHttp(route.path)) {
// router.addRoute(route); // 动态添加可访问路由表
// }
// });
// next({ ...to, replace: true });
// });
// });
// } else {
// next();
// }
// }
// } else {
// // 没有token
// if (whiteList.indexOf(to.path) !== -1) {
// // 在免登录白名单,直接进入
// next();
// } else {
// next(`/login?redirect=${to.fullPath}`); // 否则全部重定向到登录页
// NProgress.done();
// }
// }
// });
router.afterEach(() => {
NProgress.done();
});
+116
View File
@@ -0,0 +1,116 @@
import { App } from "vue";
import QomoPlatform, { QomoPlatformInstallOptions } from "@qomo-platform/core";
import { getDictCode } from "@/api/index";
export const qomoPlatformOptions = {
//字典配置
dict: {
//静态字典
staticDictMaps: {
stageData: [
{
label: "初始",
value: "A"
},
{
label: "量产",
value: "B"
},
{
label: "首批",
value: "C"
}
],
modelStatus: [
{
label: "草稿",
value: "1"
},
{
label: "已发布",
value: "2"
},
{
label: "运行中",
value: "3"
},
{
label: "已完成",
value: "4"
}
],
feeTypeList:[
{
label: "机器折旧费",
value: "EquipmentFee"
},
{
label: "辅料费",
value: "SupplyMaterialFee"
},
{
label: "水电费",
value: "DriverFee"
},
{
label: "其他费用",
value: "OtherFee"
},
{
label: "物流费",
value: "LogisticsFee"
}
],
statusList:[
{
label: "未执行",
value: "0"
},
{
label: "计算中",
value: "1"
},
{
label: "计算完成",
value: "2"
},
{
label: "出错中止",
value: "3"
},
]
},
async provider(dictType) {
const result = await getDictCode({
dictCode: dictType
});
return result.map(item =>
Object.assign(item, {
label: item.itemText,
value: item.itemValue
})
);
}
},
component: {
//配置全局文件上传
async fileUpload(options) {
const formData = new FormData();
formData.append("object", options.file);
return await uploadFile(formData);
},
getFileDetail: async files => ({
...files,
relativePath: files.name,
name: files.originalName,
url: location.origin + files?.fileurl
}),
namespace: "testApp"
}
} as QomoPlatformInstallOptions;
export default {
install(app: App) {
app.use(QomoPlatform, qomoPlatformOptions);
}
};
+60
View File
@@ -0,0 +1,60 @@
import useUserStore from "@/store/modules/user";
function authPermission(permission) {
const all_permission = "*:*:*";
const permissions = useUserStore().permissions;
if (permission && permission.length > 0) {
return permissions.some(v => {
return all_permission === v || v === permission;
});
} else {
return false;
}
}
function authRole(role) {
const super_admin = "admin";
const roles = useUserStore().roles;
if (role && role.length > 0) {
return roles.some(v => {
return super_admin === v || v === role;
});
} else {
return false;
}
}
export default {
// 验证用户是否具备某权限
hasPermi(permission) {
return authPermission(permission);
},
// 验证用户是否含有指定权限,只需包含其中一个
hasPermiOr(permissions) {
return permissions.some(item => {
return authPermission(item);
});
},
// 验证用户是否含有指定权限,必须全部拥有
hasPermiAnd(permissions) {
return permissions.every(item => {
return authPermission(item);
});
},
// 验证用户是否具备某角色
hasRole(role) {
return authRole(role);
},
// 验证用户是否含有指定角色,只需包含其中一个
hasRoleOr(roles) {
return roles.some(item => {
return authRole(item);
});
},
// 验证用户是否含有指定角色,必须全部拥有
hasRoleAnd(roles) {
return roles.every(item => {
return authRole(item);
});
}
};
+77
View File
@@ -0,0 +1,77 @@
const sessionCache = {
set(key, value) {
if (!sessionStorage) {
return;
}
if (key != null && value != null) {
sessionStorage.setItem(key, value);
}
},
get(key) {
if (!sessionStorage) {
return null;
}
if (key == null) {
return null;
}
return sessionStorage.getItem(key);
},
setJSON(key, jsonValue) {
if (jsonValue != null) {
this.set(key, JSON.stringify(jsonValue));
}
},
getJSON(key) {
const value = this.get(key);
if (value != null) {
return JSON.parse(value);
}
},
remove(key) {
sessionStorage.removeItem(key);
}
};
const localCache = {
set(key, value) {
if (!localStorage) {
return;
}
if (key != null && value != null) {
localStorage.setItem(key, value);
}
},
get(key) {
if (!localStorage) {
return null;
}
if (key == null) {
return null;
}
return localStorage.getItem(key);
},
setJSON(key, jsonValue) {
if (jsonValue != null) {
this.set(key, JSON.stringify(jsonValue));
}
},
getJSON(key) {
const value = this.get(key);
if (value != null) {
return JSON.parse(value);
}
},
remove(key) {
localStorage.removeItem(key);
}
};
export default {
/**
* 会话级缓存
*/
session: sessionCache,
/**
* 本地缓存
*/
local: localCache
};
+12
View File
@@ -0,0 +1,12 @@
import tab from "./tab";
import auth from "./auth";
import cache from "./cache";
export default function installPlugins(app) {
// 页签操作
app.config.globalProperties.$tab = tab;
// 认证对象
app.config.globalProperties.$auth = auth;
// 缓存对象
app.config.globalProperties.$cache = cache;
}
+73
View File
@@ -0,0 +1,73 @@
import useTagsViewStore from "@/store/modules/tagsView";
import router from "@/router";
export default {
// 刷新当前tab页签
refreshPage(obj) {
const { path, query, matched } = router.currentRoute.value;
if (obj === undefined) {
matched.forEach(m => {
if (m.components && m.components.default && m.components.default.name) {
if (!["Layout", "ParentView"].includes(m.components.default.name)) {
obj = { name: m.components.default.name, path: path, query: query };
}
}
});
}
return useTagsViewStore()
.delCachedView(obj)
.then(() => {
const { path, query } = obj;
router.replace({
path: "/redirect" + path,
query: query
});
});
},
// 关闭当前tab页签,打开新页签
closeOpenPage(obj) {
useTagsViewStore().delView(router.currentRoute.value);
if (obj !== undefined) {
return router.push(obj);
}
},
// 关闭指定tab页签
closePage(obj) {
if (obj === undefined) {
return useTagsViewStore()
.delView(router.currentRoute.value)
.then(({ visitedViews }) => {
const latestView = visitedViews.slice(-1)[0];
if (latestView) {
return router.push(latestView.fullPath);
}
return router.push("/");
});
}
return useTagsViewStore().delView(obj);
},
// 关闭所有tab页签
closeAllPage() {
return useTagsViewStore().delAllViews();
},
// 关闭左侧tab页签
closeLeftPage(obj) {
return useTagsViewStore().delLeftTags(obj || router.currentRoute.value);
},
// 关闭右侧tab页签
closeRightPage(obj) {
return useTagsViewStore().delRightTags(obj || router.currentRoute.value);
},
// 关闭其他tab页签
closeOtherPage(obj) {
return useTagsViewStore().delOthersViews(obj || router.currentRoute.value);
},
// 打开tab页签
openPage(url) {
return router.push(url);
},
// 修改tab页签
updatePage(obj) {
return useTagsViewStore().updateVisitedView(obj);
}
};
+285
View File
@@ -0,0 +1,285 @@
import { createWebHistory, createRouter } from "vue-router";
/* Layout */
import Layout from "@/layout";
import NProgress from "nprogress";
import usePermissionStore from "@/store/modules/permission";
import { isHttp } from "@/utils/validate";
import { getToken } from "@/utils/auth";
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";
/**
* Note: 路由配置项
*
* hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
* alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
* // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
* // 若你想不管路由下面的 children 声明的个数都显示你的根路由
* // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
* redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
* name:'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
* query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
* roles: ['admin', 'common'] // 访问路由的角色权限
* permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限
* meta : {
noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg
breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示
activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。
}
*/
// 公共路由
export const constantRoutes = [
{
path: "/redirect",
component: Layout,
hidden: true,
children: [
{
path: "/redirect/:path(.*)",
component: () => import("@/pages/redirect/index.vue")
}
]
},
{
path: "/login",
component: () => import("@/pages/Login"),
hidden: true
},
{
path: "/401",
component: () => import("@/pages/error/401"),
hidden: true
},
{
path: "",
redirect: "/index"
},
{
path: "",
component: Layout,
redirect: "/index",
children: [
{
path: "/index",
component: () => import("@/views/index"),
name: "Index",
meta: { title: "首页", icon: "home", affix: true }
},
]
},
{
path: '',
component: Layout,
children: [
{
path: "/hour",
component: () => import("@/views/hour/index"),
name: "Hour",
meta: { title: "小时费率", icon: "money", affix: true }
}
]
},
// {
// path: '',
// component: Layout,
// children: [
// {
// path: "/unitPrice",
// component: () => import("@/views/unitPrice/index"),
// name: "UnitPrice",
// meta: { title: "7525单价", icon: "money", affix: true }
// }
// ]
// }
{
path: '',
component: Layout,
children: [
{
path: "convolution/costCalculation",
component: () => import("@/views/convolution/costCalculation/index"),
name: "CostCalculation",
meta: { title: "成本计算", icon: "money", affix: true }
}
]
},
{
path: '',
component: Layout,
children: [
{
path: "convolution/singleLineQuery",
component: () => import("@/views/convolution/singleLineQuery/index"),
name: "SingleLineQuery",
meta: { title: "单行查询", icon: "money", affix: true }
}
]
},
{
path: '',
component: Layout,
children: [
{
path: "convolution/treeQuery",
component: () => import("@/views/convolution/treeQuery/index"),
name: "TreeQuery",
meta: { title: "结构树查询", icon: "tree", affix: true }
}
]
},
// {
// path: '',
// component: Layout,
// children: [
// {
// path: "miningAnalysis/statistics",
// component: () => import("@/views/miningAnalysis/statistics/index"),
// name: "Statistics",
// meta: { title: "挖掘统计", icon: "tree", affix: true }
// }
// ]
// },
{
path: '',
component: Layout,
children: [
{
path: "test/index",
component: () => import("@/views/test/index"),
name: "Test",
meta: { title: "测试组件", icon: "tree", affix: true }
}
]
},
{
path: '',
component: Layout,
children: [
{
path: "modelManagement/index",
component: () => import("@/views/modelManagement/index"),
name: "Modelmanagement",
meta: { title: "成本计算模型管理", icon: "tree", affix: true }
}
]
},
{
path: '',
component: Layout,
children: [
{
path: "standardComparison/index",
component: () => import("@/views/standardComparison/index"),
name: "Standardcomparison",
meta: { title: "标准成本对比", icon: "tree", affix: true }
}
]
},
{
path: '',
component: Layout,
children: [
{
path: "standardComparison/manHour",
component: () => import("@/views/standardComparison/component/manHour"),
name: "ManHour",
hidden: true,
}
]
},
{
path: '',
component: Layout,
children: [
{
path: "standardComparison/artificial",
component: () => import("@/views/standardComparison/component/artificial"),
name: "Artificial",
hidden: true,
}
]
},
{
path: '',
component: Layout,
children: [
{
path: "standardComparison/materials",
component: () => import("@/views/standardComparison/component/materials"),
name: "Materials",
hidden: true,
}
]
},
{
path: '',
component: Layout,
children: [
{
path: "stageComparison/index",
component: () => import("@/views/stageComparison/index"),
name: "StageComparison",
meta: { title: "阶段成本对比", icon: "tree", affix: true }
}
]
},
//成本挖掘
{
path: '',
component: Layout,
children: [
{
path: "costMining",
component: () => import("@/views/costMining/index"),
name: "CostMining",
meta: { title: "成本挖掘", icon: "tree", affix: true }
}
]
},
// Bom,工序,采购价格
{
path: '',
component: Layout,
children: [
{
path: "actualCost",
component: () => import("@/views/actualCost/index"),
name: "ActualCost",
meta: { title: "实际成本", icon: "tree", affix: true }
}
]
},
];
//本地调试默认路由
const defaultRoutes = [];
// 动态路由,基于用户权限动态去加载
export const dynamicRoutes = [...defaultRoutes];
const appPath = import.meta.env.VITE_APP_PATH;
const router = createRouter({
history: createWebHistory(`${appPath}/`),
routes: [...constantRoutes, ...dynamicRoutes],
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else {
return { top: 0 };
}
}
});
const whiteList = ["/login"];
let isAddRoute = false;
NProgress.configure({ showSpinner: false });
router.beforeEach((to, from, next) => {
next();
});
router.afterEach(() => {
NProgress.done();
});
export default router;
+50
View File
@@ -0,0 +1,50 @@
import { HttpRequest } from "@qomo-platform/core";
import { ElMessageBox } from "element-plus";
import { getToken } from "@/utils/auth";
export const genHttp = function (baseURL = "") {
const httpRequest =
new HttpRequest({
baseURL: baseURL,
//接口回调是否成功
isSuccess(resp) {
return resp?.code == 0 || resp?.code == 200 || resp?.success;
},
//获取成功情况下的data
getSuccessData(resp) {
return resp.result;
},
//不直接获取api响应的数据,而是获取成功后里面的data
getApiResponse: false,
loading: false,
message: false,
needToken: true,
processToken(config) {
Object.assign(config.headers, {
"X-Access-Token": getToken()
});
}
})
/**
* 配置401拦截
*/
httpRequest.addInterceptor({
responseInterceptorCatch(error) {
let response = error;
if (error) {
response = response.response;
}
//401
if (response?.status == 401) {
next(`/login`);
}
return Promise.reject(error);
}
});
return httpRequest;
};
//http
export const request = genHttp("/newEnergy");
export default request;
+47
View File
@@ -0,0 +1,47 @@
export default {
/**
* 网页标题
*/
title: import.meta.env.VITE_APP_TITLE,
/**
* 侧边栏主题 深色主题theme-dark,浅色主题theme-light
*/
sideTheme: "theme-light",
/**
* 是否系统布局配置
*/
showSettings: true,
/**
* 是否显示顶部导航
*/
topNav: false,
/**
* 是否显示 tagsView
*/
tagsView: true,
/**
* 是否固定头部
*/
fixedHeader: false,
/**
* 是否显示logo
*/
sidebarLogo: true,
/**
* 是否显示动态标题
*/
dynamicTitle: false,
/**
* @type {string | array} 'production' | ['production', 'development']
* @description Need show err logs component.
* The default is only used in the production env
* If you want to also use it in dev, you can pass ['production', 'development']
*/
errorLog: "production"
};
+141
View File
@@ -0,0 +1,141 @@
import md5 from 'md5'
//签名密钥串(前后端要一致,正式发布请自行修改)
const signatureSecret = "dd05f1c54d63749eda95f9fa6d49v442a";
export default class signMd5Utils {
/**
* json参数升序
* @param jsonObj 发送参数
*/
static sortAsc(jsonObj) {
let arr = new Array();
let num = 0;
for (let i in jsonObj) {
arr[num] = i;
num++;
}
let sortArr = arr.sort();
let sortObj = {};
for (let i in sortArr) {
sortObj[sortArr[i]] = jsonObj[sortArr[i]];
}
return sortObj;
}
/**
* @param url 请求的url,应该包含请求参数(url的?后面的参数)
* @param requestParams 请求参数(POST的JSON参数)
* @returns {string} 获取签名
*/
static getSign(url, requestParams) {
let urlParams = this.parseQueryString(url);
let jsonObj = this.mergeObject(urlParams, requestParams);
//console.log("sign jsonObj: ",jsonObj)
let requestBody = this.sortAsc(jsonObj);
console.log("sign requestBody: ",requestBody)
return md5(JSON.stringify(requestBody) + signatureSecret).toUpperCase();
}
/**
* @param url 请求的url
* @returns {{}} 将url中请求参数组装成json对象(url的?后面的参数)
*/
static parseQueryString(url) {
let urlReg = /^[^\?]+\?([\w\W]+)$/,
paramReg = /([^&=]+)=([\w\W]*?)(&|$|#)/g,
urlArray = urlReg.exec(url),
result = {};
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
//【这边条件没有encode】带条件参数例子:/sys/dict/getDictItems/sys_user,realname,id,username!='admin'%20order%20by%20create_time
let lastpathVariable = url.substring(url.lastIndexOf('/') + 1);
if(lastpathVariable.includes(",")){
if(lastpathVariable.includes("?")){
lastpathVariable = lastpathVariable.substring(0, lastpathVariable.indexOf("?"));
}
//解决Sign 签名校验失败 #2728
result["x-path-variable"] = decodeURIComponent(lastpathVariable);
}
if (urlArray && urlArray[1]) {
let paramString = urlArray[1], paramResult;
while ((paramResult = paramReg.exec(paramString)) != null) {
//数字值转为string类型,前后端加密规则保持一致
if(this.myIsNaN(paramResult[2])){
paramResult[2] = paramResult[2].toString()
}
result[paramResult[1]] = paramResult[2];
}
}
return result;
}
/**
* @returns {*} 将两个对象合并成一个
*/
static mergeObject(objectOne, objectTwo) {
if (objectTwo && Object.keys(objectTwo).length > 0) {
for (let key in objectTwo) {
if (objectTwo.hasOwnProperty(key) === true) {
//数字值转为string类型,前后端加密规则保持一致
if(this.myIsNaN(objectTwo[key])){
objectTwo[key] = objectTwo[key].toString()
}
objectOne[key] = objectTwo[key];
}
}
}
return objectOne;
}
static urlEncode(param, key, encode) {
if (param == null) return '';
let paramStr = '';
let t = typeof (param);
if (t == 'string' || t == 'number' || t == 'boolean') {
paramStr += '&' + key + '=' + ((encode == null || encode) ? encodeURIComponent(param) : param);
} else {
for (let i in param) {
let k = key == null ? i : key + (param instanceof Array ? '[' + i + ']' : '.' + i);
paramStr += this.urlEncode(param[i], k, encode);
}
}
return paramStr;
};
/**
* 接口签名用 生成header中的时间戳
* @returns {number}
*/
static getTimestamp(){
return new Date().getTime()
}
// /**
// * 获取客户端时间(签名参数 X_TIMESTAMP)
// * @returns {string}
// */
// static getDateTimeToString() {
// const date_ = new Date()
// const year = date_.getFullYear()
// let month = date_.getMonth() + 1
// let day = date_.getDate()
// if (month < 10) month = '0' + month
// if (day < 10) day = '0' + day
// let hours = date_.getHours()
// let mins = date_.getMinutes()
// let secs = date_.getSeconds()
// const msecs = date_.getMilliseconds()
// if (hours < 10) hours = '0' + hours
// if (mins < 10) mins = '0' + mins
// if (secs < 10) secs = '0' + secs
// if (msecs < 10) secs = '0' + msecs
// return year + '' + month + '' + day + '' + hours + '' + mins + '' + secs
// }
// true:数值型的,false:非数值型
static myIsNaN(value) {
return typeof value === 'number' && !isNaN(value);
}
}
+5
View File
@@ -0,0 +1,5 @@
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
const store = createPinia();
store.use(piniaPluginPersistedstate);
export default store;

Some files were not shown because too many files have changed in this diff Show More