This commit is contained in:
2023-09-13 18:29:35 +08:00
commit 4ac8391a9a
126 changed files with 15555 additions and 0 deletions

6
.env.development Normal file
View File

@@ -0,0 +1,6 @@
# 本地环境
ENV = development
# 本地环境接口地址
# VITE_API_URL = 'http://admintestapi.sxczgkj.cn/admin/'
VITE_API_URL = 'http://admintestapi.sxczgkj.cn/admin/'

5
.env.production Normal file
View File

@@ -0,0 +1,5 @@
# 线上环境
ENV = production
# 线上环境接口地址
VITE_API_URL = 'http://admintestapi.sxczgkj.cn/admin/'

View File

@@ -0,0 +1,82 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"ENUMS": true,
"EffectScope": true,
"ElMessage": true,
"InjectionKey": true,
"PropType": true,
"Ref": true,
"VNode": true,
"_hook": true,
"acceptHMRUpdate": true,
"computed": true,
"createApp": true,
"createPinia": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"defineStore": true,
"effectScope": true,
"getActivePinia": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"mapActions": true,
"mapGetters": true,
"mapState": true,
"mapStores": true,
"mapWritableState": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"setActivePinia": true,
"setMapStoreSuffix": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"storeToRefs": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useLink": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

43
README.md Normal file
View File

@@ -0,0 +1,43 @@
## 🌈 介绍
基于 vue3.x + CompositionAPI setup 语法糖 + vite + element plus + vue-router-next + pinia 技术的后台开源免费模板,希望减少工作量,实现快速开发,此项目为 JS 非 TS 版本。
### 🏭 环境支持
| Edge | Firefox | Chrome | Safari |
| --------- | ------------ | ----------- | ----------- |
| Edge ≥ 88 | Firefox ≥ 78 | Chrome ≥ 87 | Safari ≥ 13 |
> 由于 Vue3 不再支持 IE11故而 ElementPlus 也不支持 IE11 及之前版本。
### 😉 hooks
> 依赖 ElementPlus方法提示均使用 ElMessage
| 说明 | 方法 | 说明 | 使用 |
| ---- | ------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 数据 | `useDeepClone` | 深度克隆 | `const result = useDeepClone(target)`<br />`@param {Object} target`:要克隆的目标<br />`@return {Object} result`:结果 |
| 存储 | `useLocalStorage` | 会话存储 | `useLocalStorage.set(key, value)`: 设置临时缓存<br />`useLocalStorage.get(key)`: 获取临时缓存<br />`useLocalStorage.remove(key)`: 移除临时缓存<br />`useLocalStorage.clear()`: 移除全部临时缓存<br />`@param {String} key`:设置缓存的名称<br />`@param {Any} value`:设置缓存的值 |
| | `useLocalStorage` | 本地存储 | `useLocalStorage.set(key, value)`: 设置临时缓存<br />`useLocalStorage.get(key)`: 获取临时缓存<br />`useLocalStorage.remove(key)`: 移除临时缓存<br />`useLocalStorage.clear()`: 移除全部临时缓存<br />`@param {String} key`:设置缓存的名称<br />`@param {Any} value`:设置缓存的值 |
| 函数 | `useDebounce` | 防抖 | `useDebounce(function, time)`<br />`@param {Function} function`:要处理防抖的函数<br />`@param {String,Number} time`:防抖的时间<br />`@return {function}`:防抖的函数 |
| 日期 | `useDateFormat` | 日期格式 | `useDateFormat(date, [format])`<br />`@param {Date} date`:任何合法的时间格式、秒或毫秒的时间戳<br />`@param {String} format`:时间格式,可选,默认为 YYYY-mm-dd。<br />`@return {String}`:格式好的时间 |
| 验证 | `usePureNum` | 验证是否为纯数字 | `usePureNum(str)`<br />`@param {String,Number} str`:要验证的字符串或数字<br />`@return {Boolean}`true / false |
| | `usePhoneNum` | 验证是手机号 | `usePhoneNum(str)`<br />`@param {String,Number} str`:要验证的手机号<br />`@return {Boolean}`true / false |
| | `useEmailNum` | 验证是邮箱号 | `useEmailNum(str)`<br />`@param {String} str`:要验证的字符串<br />`@return {Boolean}`true / false |
| | `useEmptyObj` | 验证是空对象 | `useEmptyObj(obj)`<br />`@param {Object} obj`:要验证的对象<br />`@return {Boolean}`true / false |
| | `useDataType` | 检查数据类型 | `useDataType(data,[type])`<br />`@param {Any} data`:数据<br />`@param {Type}type`:类型,可选值。<br />如果传了 type 会返回一个布尔值表示 data 是否与 type 类型相等<br />`@return {String,Boolean}`:数据类型或 true / false |
| 样式 | `useCssVar` | 设置 css 变量 | `useCssVar(key, value)`<br />`@param {String} key`:要设置变量<br />`@param {String} value`:设置的值 |
| | `useRgbToHex` | hex 颜色转 rgb 颜色 | `useRgbToHex(r, g, b)`<br />`@param {String} r`:红色<br />`@param {String} g`:绿色<br />`@param {String} b`:蓝色<br />`@return {String}`Hex 值 |
| | `useHexToRgb` | rgb 颜色转 Hex 颜色 | `useHexToRgb(str)`<br />`@param {String} str`:颜色值字符串<br />`@return {String}`Rgb 值 |
| | `useDarkColor` | 加深颜色值 | `useDarkColor(color, level)`<br />`@param {String} color`:颜色值字符串<br />`@param {String} level`:加深的程度,限 0-1 之间<br />`@return {String}`:加深后的颜色 |
| | `useLightColor` | 变浅颜色值 | `useLightColor(color, level)`<br />`@param {String} color`:颜色值字符串<br />`@param {String} level`:变浅的程度,限 0-1 之间<br />`@return {String}`:变浅后的颜色 |
| 按键 | `useKeyStroke` | 键盘按下事件 **<br />注: 仅支持如下键 Esc、Tab、<br />BackSpace、Enter、Shift、Ctrl、<br />Alt、Up、Down、Left、Right** | `useKeyStroke(key, fun)`<br />`@param {String} key`:要监听的键<br />`@param {Function} fun`:回调函数 |
| 方法 | `useRepairZero` | 补零 ( 当数小于 10 在前补零 ) | `useRepairZero(number)`<br />`@param {Number} number`:当前数<br />`@return {Number}`:补零后的数 |
### 😊 指令
| 方法 | 说明 | 使用 |
| ------------ | ---------------------- | ----------------------- |
| `v-isLogin` | 需要登录后可操作的函数 | `v-isLogin="fun"` |
| `v-throttle` | 节流函数指令 | `v-throttle:1000="fun"` |
| `v-size-ob` | 监控元素的尺寸变化 | `v-size-ob="fun"` |

20
addVersion.js Normal file
View File

@@ -0,0 +1,20 @@
//npm run build打包前执行此段代码
import fs from 'fs'
//返回package的json数据
function getPackageJson() {
let data = fs.readFileSync('./package.json');//fs读取文件
return JSON.parse(data);//转换为json对象
}
let packageData = getPackageJson();//获取package的json
let arr = packageData.version.split('.');//切割后的版本号数组
arr[2] = parseInt(arr[2]) + 1;
packageData.version = arr.join('.');//转换为以"."分割的字符串
//用packageData覆盖package.json内容
fs.writeFile(
'./package.json',
JSON.stringify(packageData, null, "\t"
),
(err) => { }
);

16
index.html Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favico.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vue admin</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

3770
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "vue-admin",
"private": true,
"version": "1.2.23",
"type": "module",
"scripts": {
"dev": "vite",
"build": "node ./addVersion.js && vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.3.4",
"echarts": "^5.4.2",
"element-china-area-data": "^6.0.2",
"element-plus": "^2.3.0",
"fs": "^0.0.1-security",
"js-cookie": "^3.0.1",
"nprogress": "^0.2.0",
"pinia": "^2.0.33",
"vue": "^3.2.47",
"vue-clipboard3": "^2.0.0",
"vue-router": "^4.1.6",
"vue3-count-to": "^1.1.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.1.0",
"sass": "^1.59.3",
"unplugin-auto-import": "^0.15.1",
"vite": "^4.2.0"
}
}

BIN
public/favico.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

81
src/App.vue Normal file
View File

@@ -0,0 +1,81 @@
<template>
<router-view></router-view>
</template>
<script setup>
import { useConfigure } from "@/store/configure.js";
import { useRoutes } from "@/store/routes.js";
const storeConfigure = useConfigure();
const storeRoutes = useRoutes();
const route = useRoute();
// 获取枚举值
const { layoutModeEnum } = ENUMS;
watch(
route,
(newValue) => {
const componentName = _hook.useComponentName(newValue);
if (newValue?.meta?.isKeepAlive && componentName) {
storeRoutes.setCachedRoute(componentName);
}
storeRoutes.setBreadcrumb(newValue.matched.slice(1));
document.title = route?.meta?.title || route.path;
},
{ deep: true }
);
onMounted(() => {
// 初始化配置
const configure = _hook.useLocalStorage.get("configure");
if (configure) {
storeConfigure.initConfigure(configure);
}
// 初始化颜色
_hook.useCssVar("--el-color-primary", storeConfigure.configure.themeColor);
switch (storeConfigure.configure.menuMode) {
case layoutModeEnum.key[0]:
_hook.useCssVar("--el-menu-bg-color", storeConfigure.configure.menuBGColor);
break;
case layoutModeEnum.key[1]:
_hook.useCssVar("--el-menu-bg-color", "#ffffff");
break;
default:
}
_hook.useCssVar("--admin-column-bg-color", storeConfigure.configure.menuBGColor);
_hook.useCssVar("--el-menu-text-color", storeConfigure.configure.textColor);
_hook.useCssVar("--el-menu-active-color", storeConfigure.configure.activeTextColor);
[3, 5, 7, 8, 9].forEach((i) => {
_hook.useCssVar(`--el-color-primary-light-${i}`, _hook.useLightColor(storeConfigure.configure.themeColor, `0.${i}`));
});
// 面包屑导航
storeRoutes.setBreadcrumb(route.matched.slice(1));
});
</script>
<style lang="scss">
* {
box-sizing: border-box;
}
// 自定义进度条颜色
#nprogress .bar {
height: 5px !important;
background: var(--el-color-primary-light-3) !important;
}
.mt15 {
margin-top: 15px;
}
.pb15 {
padding-bottom: 15px;
}
.card {
background-color: #fff;
padding: 15px;
border-radius: 4px;
}
</style>

24
src/api/device.js Normal file
View File

@@ -0,0 +1,24 @@
import request from "@/utils/request.js";
/**
* 当前用户设备总数
* @returns
*/
export function getSumDeviceStock() {
return request({
method: "get",
url: "/agency/getSumDeviceStock"
});
}
/**
* 获取设备列表
* @returns
*/
export function getDeviceStockInfo(params) {
return request({
method: "get",
url: "/agency/getDeviceStockInfo",
params
});
}

88
src/api/home.js Normal file
View File

@@ -0,0 +1,88 @@
import request from "@/utils/request.js";
import { dayjs } from 'element-plus';
/**
* 获取首页数据
* @returns
*/
export function getIndexData() {
return request({
method: "POST",
url: "/user/getIndexData"
});
}
/**
* 获取首页图表数据
* @returns
*/
export function getChartData(type, reqUserId) {
const m = {
1: '/user/getDayOrder', // 获取近七日收单额
2: '/user/getDayProfit', // 获取近七日收益
3: '/user/getMonthOrder', // 获取近一年的收单额
4: '/user/getMonthProfit' // 获取近一年的收益数据
}
const params = {
year: dayjs().format('YYYY'),
reqUserId: reqUserId
}
return request({
method: "GET",
url: m[type],
params
});
}
/**
* 上传OSS
* @param {*} params
* @returns
*/
export function uploadOSS(data) {
// console.log('data', data)
let formData = new FormData()
formData.append('file', data)
console.log('formData', formData)
return request({
method: 'post',
url: '/promotion/OSSUpdate',
data: formData
})
}
/**
* app菜单list
* @param {*} params
* @returns
*/
export function appMenuPage(params) {
return request({
method: 'GET',
url: '/AppMenu/page',
params
})
}
/**
* 通过code查询
* @param {*} code
* @returns
*/
export function getDictGroup(code) {
return request({
method: 'GET',
url: `/dict/getGroup/${code}`
})
}
/**
* 增加菜单
* @returns
*/
export function appMenuSave(data) {
return request({
method: "POST",
url: "/AppMenu/save",
data
});
}

97
src/api/organization.js Normal file
View File

@@ -0,0 +1,97 @@
// 机构管理接口
import request from "@/utils/request.js";
/**
* 创建机构
* @returns
*/
export function addAgency(data) {
return request({
method: "POST",
url: "/agency/addAgency",
data
});
}
/**
* 机构列表
* @returns
*/
export function queryAgency(params) {
return request({
method: "get",
url: "/agency/queryAgency",
params
});
}
/**
* 修改下级费率
* @returns
*/
export function modifyFee(params) {
return request({
method: "get",
url: "/agency/modifyFee",
params
});
}
/**
* 获取收益信息
* @returns
*/
export function queryProfit(params) {
return request({
method: "get",
url: "/agency/queryProfit",
params
});
}
/**
* 获取收益信息 商家列表
* @returns
*/
export function queryOrder(params) {
return request({
method: "get",
url: "/agency/queryOrder",
params
});
}
/**
* 查询用户总收益和可提收益,冻结收益 ,已提收益
* @returns
*/
export function getUserBalance() {
return request({
method: "get",
url: "/user/getUserBalance"
});
}
/**
* 提现
* @returns
*/
export function withdrawalProfit(params) {
return request({
method: "get",
url: "/user/withdrawalProfit",
params
});
}
/**
* 获取提现流水
* @returns
*/
export function getUserOutFlow(params) {
return request({
method: "get",
url: "/user/getUserOutFlow",
params
});
}

37
src/api/promotion.js Normal file
View File

@@ -0,0 +1,37 @@
import request from "@/utils/request.js"
/**
* 推广宽图列表
* @returns
*/
export function promotionImageList(params) {
return request({
method: "get",
url: "/promotion/promotionImageList",
params
});
}
/**
* 增加图片
* @returns
*/
export function insert(data) {
return request({
method: "POST",
url: "/promotion/insert",
data
});
}
/**
* 更改推广宽图
* @returns
*/
export function updateById(data) {
return request({
method: "POST",
url: "/promotion/updateById",
data
});
}

49
src/api/setting.js Normal file
View File

@@ -0,0 +1,49 @@
import request from "@/utils/request.js"
/**
* 获取appid 信息
* @param {*} params
* @returns
*/
export function querySystemApis(params) {
return request({
method: 'GET',
url: '/systemApi/querySystemApis',
params
})
}
/**
* 生成密钥对
* @returns
*/
export function createKey() {
return request({
method: 'GET',
url: '/systemApi/createKey'
})
}
/**
* 新增api
* @returns
*/
export function initApi(params) {
return request({
method: 'GET',
url: '/systemApi/initApi',
params
})
}
/**
* 修改api 配置
* @returns
*/
export function modfityApi(params) {
return request({
method: 'GET',
url: '/systemApi/modfityApi',
params
})
}

162
src/api/shop.js Normal file
View File

@@ -0,0 +1,162 @@
import request from "@/utils/request.js";
/**
* 获取商户列表
* @returns
*/
export function queryCustormerFlow(params) {
return request({
method: "get",
url: "/agency/queryCustormerFlow",
params
});
}
/**
* 商户列表数据统计
* @returns
*/
export function queryCustormSum() {
return request({
method: "get",
url: "/agency/queryCustormSum"
});
}
/**
* 实名认证信息
* @returns
*/
export function merchantInfoDetail(userId) {
return request({
method: "get",
url: `/merchantInfo/detail/audit/${userId}`
});
}
/**
* 获取创客审核列表
* @param {*} params
* @returns
*/
export function getUserMark(params) {
return request({
method: "get",
url: `/agency/getUserMark`,
params
});
}
/**
* 审核创客申请审核
* @param {*} params
* @returns
*/
export function updateUserMark(params) {
return request({
method: "get",
url: `/agency/updateUserMark`,
params
});
}
/**
* 根据城市获取对应的支行
* @param {*} params
* @returns
*/
export function getBranchList(params) {
return request({
method: 'GET',
url: '/merchantInfo/getBranchList',
params
})
}
/**
* 更改实名认证信息
* @param {*} data
* @returns
*/
export function updatePromoterInformation(data) {
return request({
method: 'post',
url: '/merchantInfo/updatePromoterInformation',
data
})
}
/**
* mcc相关
* @param {*} params
* @returns
*/
export function mccPageData(params) {
return request({
method: 'GET',
url: '/merchantInfo/mccPageData',
params
})
}
/**
* 商户基本信息
* @param {*} params
* @returns
*/
export function merchBaseInfo(userId) {
return request({
method: 'GET',
url: `/merchantInfo/detail/merchBaseInfo/${userId}`
})
}
/**
* 修改商户相关信息
* @param {*} data
* @returns
*/
export function updateMerchantInformation(data) {
return request({
method: 'post',
url: '/merchantInfo/updateMerchantInformation',
data
})
}
/**
* 实名认证信息页面(实名个数)
* @param {*} data
* @returns
*/
export function connectInfo(userId) {
return request({
method: 'get',
url: `/merchantInfo/connectInfo/${userId}`
})
}
/**
* 结算信息
* @param {*} data
* @returns
*/
export function merchBaseAccount(userId) {
return request({
method: 'get',
url: `/merchantInfo/detail/merchBaseAccount/${userId}`
})
}
/**
* 更改结算信息
* @param {*} data
* @returns
*/
export function updateAccount(data) {
return request({
method: 'post',
url: '/merchantInfo/detail/updateAccount',
data
})
}

15
src/api/user.js Normal file
View File

@@ -0,0 +1,15 @@
import request from "@/utils/request.js";
export function login(data) {
return request({
method: "POST",
url: "/user/doLogin",
data,
});
}
export function getUserInfo() {
return request({
method: "POST",
url: "/user/getUserInfoByToken",
});
}

25
src/api/withdraw.js Normal file
View File

@@ -0,0 +1,25 @@
import request from "@/utils/request.js";
/**
* 提现申请查询
* @returns
*/
export function getOutFlow(params) {
return request({
method: "get",
url: "/user/getOutFlow",
params
});
}
/**
* 提现审核
* @returns
*/
export function modifyOutFlow(params) {
return request({
method: "get",
url: "/user/modifyOutFlow",
params
});
}

BIN
src/assets/home_icon1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/assets/home_icon2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/assets/home_icon3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/assets/home_icon4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
src/assets/home_icon5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/assets/home_icon6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
src/assets/home_icon7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
src/assets/home_icon8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
src/assets/icon_dev1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
src/assets/icon_dev2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
src/assets/icon_dev3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
src/assets/images/404.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src/assets/logo_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

View File

@@ -0,0 +1,21 @@
<template>
<el-icon v-if="name" :size="size" :color="color">
<component :is="name"></component>
</el-icon>
</template>
<script setup>
const props = defineProps({
name: {
type: String,
required: true,
default: "",
},
size: {
type: [String, Number],
},
color: {
type: String,
},
});
</script>

View File

@@ -0,0 +1,36 @@
<template>
<el-cascader :placeholder="placeholder" :options="regionData" v-model="selectedOptions" @change="change"></el-cascader>
</template>
<script setup>
import { regionData, codeToText } from 'element-china-area-data'
import { defineEmits, defineExpose } from 'vue'
const emit = defineEmits(['change'])
const placeholder = ref('')
const selectedOptions = ref([])
// 选择区域
function change(e) {
emit('change', [{
code: selectedOptions.value[0],
label: codeToText[selectedOptions.value[0]]
}, {
code: selectedOptions.value[1],
label: codeToText[selectedOptions.value[1]]
}, {
code: selectedOptions.value[2],
label: codeToText[selectedOptions.value[2]]
}])
}
// 设置默认值
function setValue(arr) {
selectedOptions.value = arr
}
defineExpose({
placeholder,
setValue
})
</script>

View File

@@ -0,0 +1,226 @@
<template>
<div class="chart_header">
<div class="item" :class="{ active1: chartType == 1 }" @click="chartTypeChange(1)">
<div class="icon"></div>
<span>收益</span>
</div>
<div class="item" :class="{ active2: chartType == 2 }" @click="chartTypeChange(2)">
<div class="icon"></div>
<span>流水</span>
</div>
</div>
<div class="chart_wrap" v-loading="chartLoading">
<div ref="lineRef1" class="item" style="height: 600px;"></div>
<div ref="lineRef2" class="item" style="height: 600px;"></div>
</div>
</template>
<script setup>
import { getChartData } from '@/api/home.js'
import * as echarts from "echarts"
import { dayjs } from 'element-plus'
import hooks from '@/hooks'
// 定义变量内容
const lineRef1 = ref();
const lineRef2 = ref();
const chartsObj = {
lineRef1: '',
lineRef2: '',
myCharts: [],
};
const chartLoading = ref(true)
const chartType = ref(1)
const chartYear = ref(dayjs().format('YYYY'))
const props = defineProps({
userId: {
type: [String, Number],
default: hooks.useLocalStorage.get('userInfo').userId
}
})
// 初始化折线图
function initDeicount(yearData = [], sevenData = [], sevenDataTime = []) {
if (!chartsObj.lineRef1 && !chartsObj.lineRef2) {
chartsObj.lineRef1 = echarts.init(lineRef1.value);
chartsObj.lineRef2 = echarts.init(lineRef2.value);
}
const option1 = {
title: {
text: chartType.value == 1 ? '年度收益' : '年度流水',
x: 'center',
y: '95%'
},
tooltip: {
trigger: 'axis'
},
xAxis: [
{
type: 'category',
data: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二'],
axisPointer: {
type: 'shadow'
},
axisLabel: {
interval: 0
}
}
],
yAxis: {
type: "value"
},
series: [
{
type: 'bar',
data: yearData,
barWidth: '20',
itemStyle: {
color: chartType.value == 1 ? '#5470C6' : '#91CB75'
}
}
]
};
const option2 = {
title: {
text: chartType.value == 1 ? '七日收益' : '七日流水',
x: 'center',
y: '95%'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: "category",
data: sevenDataTime
},
yAxis: {
type: "value",
},
series: [
{
type: "line",
data: sevenData,
itemStyle: {
color: chartType.value == 1 ? '#5470C6' : '#91CB75'
}
}
]
};
chartsObj.lineRef1.setOption(option1)
chartsObj.lineRef2.setOption(option2)
chartsObj.myCharts.push(chartsObj.lineRef1)
chartsObj.myCharts.push(chartsObj.lineRef2)
}
// 改变chart类型
function chartTypeChange(t) {
chartType.value = t
chartLoading.value = true
getChartDataMethod()
}
// 获取chart数据
async function getChartDataMethod() {
try {
let yearData = ''
let sevenData = ''
let sevenDataTime = ''
if (chartType.value == 1) {
// 收益
const res1 = await getChartData(4, props.userId)
const res2 = await getChartData(2, props.userId)
yearData = res1.map(item => item.price)
sevenData = res2.map(item => item.price)
sevenDataTime = res2.map(item => dayjs(item.times).format('MM/DD'))
} else {
// 流水
const res1 = await getChartData(3, props.userId)
const res2 = await getChartData(1, props.userId)
yearData = res1.map(item => item.consumeFee)
sevenData = res2.map(item => item.consumeFee)
sevenDataTime = res2.map(item => dayjs(item.times).format('MM/DD'))
}
initDeicount(yearData, sevenData, sevenDataTime)
chartLoading.value = false
} catch (error) { }
}
// 批量设置 echarts resize
function initEChartsResize() {
var myEvent = new Event("resize");
window.addEventListener("resize", () => {
nextTick(() => {
for (let i = 0; i < chartsObj.myCharts.length; i++) {
setTimeout(() => {
chartsObj.myCharts[i].resize();
}, 1000);
}
});
});
window.dispatchEvent(myEvent);
}
onMounted(() => {
getChartDataMethod()
})
onActivated(() => {
initEChartsResize();
});
</script>
<style scoped lang="scss">
.chart_header {
display: flex;
justify-content: center;
padding-top: 20px;
.item {
display: flex;
align-items: center;
&:last-child {
margin-left: 20px;
}
&:hover {
cursor: pointer;
}
&.active1 {
.icon {
background-color: #5470C6;
}
}
&.active2 {
.icon {
background-color: #91CB75;
}
}
.icon {
width: 40px;
height: 20px;
border-radius: 2px;
background-color: #ccc;
}
span {
font-size: 14px;
color: #999;
margin-left: 8px;
}
}
}
.chart_wrap {
display: flex;
padding-bottom: 20px;
.item {
flex: 1;
}
}
</style>

98
src/components/editor.vue Normal file
View File

@@ -0,0 +1,98 @@
<template>
<div class="editor-container">
<Toolbar :editor="editorRef" :mode="mode" />
<Editor :mode="mode" :defaultConfig="state.editorConfig" :style="{ height }" v-model="state.editorVal" @onCreated="handleCreated" @onChange="handleChange" />
</div>
</template>
<script setup>
// https://www.wangeditor.com/v5/for-frame.html#vue3
import "@wangeditor/editor/dist/css/style.css";
import { Toolbar, Editor } from "@wangeditor/editor-for-vue";
// 定义父组件传过来的值
const props = defineProps({
// 是否禁用
disable: {
type: Boolean,
default: () => false,
},
// 内容框默认 placeholder
placeholder: {
type: String,
default: () => "请输入内容...",
},
// 模式,可选 <default|simple>,默认 default
mode: {
type: String,
default: () => "default",
},
// 高度
height: {
type: String,
default: () => "300px",
},
// 双向绑定,用于获取 editor.getHtml()
getHtml: String,
// 双向绑定,用于获取 editor.getText()
getText: String,
});
// 定义子组件向父组件传值/事件
const emit = defineEmits(["update:getHtml", "update:getText"]);
// 定义变量内容
const editorRef = shallowRef();
const state = reactive({
editorConfig: {
placeholder: props.placeholder,
},
editorVal: props.getHtml,
});
// 编辑器回调函数
const handleCreated = (editor) => {
editorRef.value = editor; // 记录 editor 实例,重要!
};
// 编辑器内容改变时
const handleChange = (editor) => {
emit("update:getHtml", editor.getHtml());
emit("update:getText", editor.getText());
};
nextTick(() => {
// 监听是否禁用改变
watch(
() => props.disable,
(bool) => {
const editor = editorRef.value;
if (editor == null) return;
bool ? editor.disable() : editor.enable();
},
{ immediate: true }
);
});
// 监听双向绑定值改变,用于回显
watch(
() => props.getHtml,
(val) => {
state.editorVal = val;
}
);
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value;
if (editor == null) return;
editor.destroy();
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,99 @@
<template>
<el-dialog v-model="showDialog" title="累计收益" width="80%">
<el-space>
<el-input placeholder="请输入订单号搜索" v-model="tableOptions.orderNumber" style="width: 200px;" />
<el-input placeholder="请输入商户号搜索" v-model="tableOptions.merchantCode" style="width: 200px;" />
<el-button type="primary" icon="Search" @click="searchHandle">搜索</el-button>
</el-space>
<div class="mt15">
<el-table :data="tableOptions.list" v-loading="tableOptions.loading">
<el-table-column prop="orderNumber" label="订单号"></el-table-column>
<el-table-column prop="merchantCode" label="商户号"></el-table-column>
<el-table-column prop="price" label="收益额"></el-table-column>
<el-table-column prop="current_fee" label="推广费率">
<template #default="scope">
<el-text type="primary">{{ scope.row.currentFee }}%</el-text>
</template>
</el-table-column>
<el-table-column prop="loginName" label="名称">
<template #default="scope">
<el-text>{{ scope.row.loginName }}</el-text>
<el-tag disable-transitions style="margin-left: 8px;">{{ typeNames[scope.row.typeCode] }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="merchantName" label="商户名称">
</el-table-column>
<el-table-column prop="createDt" label="时间">
<template #default="scope">
{{ dayjs(scope.row.createDt).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
</el-table>
</div>
<div class="mt15">
<el-pagination layout="prev, pager, next, total, sizes, jumper" v-model:current-page="tableOptions.pageNum"
v-model:page-size="tableOptions.pageSzie" :page-size="tableOptions.pageSzie" :page-sizes="[10, 20, 30, 50]"
:total="tableOptions.total" @size-change="paginationChange" @current-change="paginationChange" />
</div>
</el-dialog>
</template>
<script setup>
import { dayjs } from 'element-plus';
import { typeNames } from '@/utils/index.js'
import { queryProfit } from '@/api/organization.js'
const showDialog = ref(false)
// 表格参数
const tableOptions = reactive({
loading: true,
reqUserId: '', // 上级id
orderNumber: '', // 订单号
merchantCode: '', // 商户号
list: [],
total: 0,
pageNum: 1,
pageSzie: 10
})
// 搜索
function searchHandle() {
tableOptions.pageNum = 1;
paginationChange()
}
// 分页回调
function paginationChange() {
tableOptions.loading = true
getTableDate()
}
// 获取机构列表
async function getTableDate() {
try {
const res = await queryProfit({
reqUserId: tableOptions.reqUserId,
orderNumber: tableOptions.orderNumber,
merchantCode: tableOptions.merchantCode,
pageNum: tableOptions.pageNum,
pageSzie: tableOptions.pageSzie
})
tableOptions.loading = false
tableOptions.list = res.list
tableOptions.total = res.total
} catch (error) { }
}
const show = (reqUserId) => {
tableOptions.reqUserId = reqUserId
showDialog.value = true
getTableDate()
}
defineExpose({
show
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,125 @@
<template>
<canvas id="canvas" :width="props.width" :height="props.height" @click="handleCanvas"> </canvas>
</template>
<script setup>
const props = defineProps({
width: {
type: Number,
default: 100,
},
height: {
type: Number,
default: 38,
},
quantity: {
type: Number,
default: 4,
},
line: {
type: Number,
default: 10,
},
spot: {
type: Number,
default: 50,
},
});
let trueCode = ref(""); //保存正确的验证码
// 只用于传参,并且数组长度不能「多于」下面验证码遍历的次数,不然最终得到的 trueCode 会有问题
let verificationCode = [];
function draw(showCode) {
var canvas_width = document.querySelector("#canvas").clientWidth;
var canvas_height = document.querySelector("#canvas").clientHeight;
var canvas = document.getElementById("canvas"); // 获取到canvas
var context = canvas.getContext("2d"); // 获取到canvas画图
canvas.width = canvas_width;
canvas.height = canvas_height;
var sCode = "a,b,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,E,F,G,H,J,K,L,M,N,P,Q,R,S,T,W,X,Y,Z,1,2,3,4,5,6,7,8,9,0";
var aCode = sCode.split(",");
var aLength = aCode.length; // 获取到数组的长度
// 验证码个数
for (var i = 0; i < props.quantity; i++) {
var j = Math.floor(Math.random() * aLength); // 获取到随机的索引值
var deg = (Math.random() * 30 * Math.PI) / 180; // 产生0~30之间的随机弧度
var txt = aCode[j]; // 得到随机的一个内容
showCode[i] = txt.toLowerCase(); // 依次把取得的内容放到数组里面
var x = 10 + i * 20; // 文字在canvas上的x坐标
var y = 20 + Math.random() * 8; // 文字在canvas上的y坐标
context.font = "bold 23px 微软雅黑";
context.translate(x, y);
context.rotate(deg);
context.fillStyle = randomColor();
context.fillText(txt, 0, 0);
context.rotate(-deg);
context.translate(-x, -y);
}
// 验证码上显示的线条
for (var i = 0; i < props.line; i++) {
context.strokeStyle = randomColor();
context.beginPath();
context.moveTo(Math.random() * canvas_width, Math.random() * canvas_height);
context.lineTo(Math.random() * canvas_width, Math.random() * canvas_height);
context.stroke();
}
// 验证码上显示的小点
for (var i = 0; i < props.spot; i++) {
context.strokeStyle = randomColor();
context.beginPath();
var x = Math.random() * canvas_width;
var y = Math.random() * canvas_height;
context.moveTo(x, y);
context.lineTo(x + 1, y + 1);
context.stroke();
}
// 最后把取得的验证码数组存起来,方式不唯一
var num = showCode.join("");
trueCode.value = num;
}
// 得到随机的颜色值
function randomColor() {
var r = Math.floor(Math.random() * 256);
var g = Math.floor(Math.random() * 256);
var b = Math.floor(Math.random() * 256);
return "rgb(" + r + "," + g + "," + b + ")";
}
const emits = defineEmits(["getCode"]);
// canvas 点击刷新
function handleCanvas() {
draw(verificationCode);
emits("getCode", trueCode.value);
}
onMounted(() => {
draw(verificationCode);
emits("getCode", trueCode.value);
});
</script>
<style lang="scss" scoped>
#canvas {
margin-right: 1%;
display: block;
border: 1px solid #ccc;
border-radius: 5px;
cursor: pointer;
margin-right: 80px;
}
.input-val {
width: 50%;
background: #ffffff;
height: 2.8rem;
border-radius: 5px;
border: none;
padding: 0 0 0 12px;
border: 1px solid rgba(0, 0, 0, 0.2);
}
</style>

13
src/directive/index.js Normal file
View File

@@ -0,0 +1,13 @@
import isLogin from "./isLogin.js";
import throttle from "./throttle.js";
import sizeOb from "./sizeOb.js";
import permission from './permission'
const initDirective = {
install(app) {
app.directive("isLogin", isLogin);
app.directive("throttle", throttle);
app.directive("sizeOb", sizeOb);
app.directive("permission", permission);
},
};
export default initDirective;

48
src/directive/isLogin.js Normal file
View File

@@ -0,0 +1,48 @@
import _hook from "@/hooks/index.js";
import { ElMessage } from "element-plus";
export default {
mounted(el, binding) {
if (!_hook.useDataType(binding.value, "function")) {
ElMessage({
message: "v-isLogin 请绑定函数",
type: "warning",
});
return;
}
el.handle = () => {
if (!_hook.useLocalStorage.get("token")) {
ElMessage({
message: "请登录后在操作",
type: "warning",
});
return;
}
binding.value();
};
el.addEventListener("click", el.handle);
},
updated() {
if (!_hook.useDataType(binding.value, "function")) {
ElMessage({
message: "v-isLogin 请绑定函数",
type: "warning",
});
return;
}
el.removeEventListener("click", el.handler);
el.handle = () => {
if (!_hook.useLocalStorage.get("token")) {
ElMessage({
message: "请登录后在操作.",
type: "warning",
});
return;
}
binding.value();
};
el.addEventListener("click", el.handle);
},
unmounted(el) {
el.removeEventListener("click", el.handler);
},
};

View File

@@ -0,0 +1,19 @@
import _hook from "@/hooks/index.js";
import { useUser } from "@/store/user.js";
const permission = async (el, bindings) => {
const storeUser = useUser();
// 触发初始化用户信息
await storeUser.setUserInfo();
const roles = storeUser.userInfo.userType;
// 匹配权限,无权择删除按钮
if (!bindings.value.includes(roles)) {
if (el.parentNode) {
// console.log('el===', el)
// console.log('parentNode===', el.parentNode)
el.parentNode.removeChild(el)
}
}
}
export default permission

22
src/directive/sizeOb.js Normal file
View File

@@ -0,0 +1,22 @@
const map = new WeakMap();
const ob = new ResizeObserver((entries) => {
for (const entry of entries) {
const handler = map.get(entry.target);
if (handler) {
const box = entry.borderBoxSize[0];
handler({
width: box.inlineSize,
height: box.blockSize,
});
}
}
});
export default {
mounted(el, binding) {
ob.observe(el);
map.set(el, binding.value);
},
unmounted(el) {
ob.unobserve(el);
},
};

65
src/directive/throttle.js Normal file
View File

@@ -0,0 +1,65 @@
import _hook from "@/hooks/index.js";
import { ElMessage } from "element-plus";
export default {
mounted(el, binding) {
if (!_hook.useDataType(binding?.value, "function")) {
ElMessage({
message: "v-throttle 请绑定函数",
type: "warning",
});
return;
}
if (!_hook.usePureNum(binding?.arg)) {
ElMessage({
message: "v-throttle 指令参数仅支持传入纯数字",
type: "warning",
});
return;
}
el.handler = () => {
el.classList.add("is-disabled");
el.disabled = true;
if (el.disabled) {
binding.value();
setTimeout(() => {
el.classList.remove("is-disabled");
el.disabled = false;
}, binding.arg);
}
};
el.addEventListener("click", el.handler);
},
updated(el, binding) {
if (!_hook.useDataType(binding?.value, "function")) {
ElMessage({
message: "v-throttle 请绑定函数",
type: "warning",
});
return;
}
if (!_hook.usePureNum(binding?.arg)) {
ElMessage({
message: "v-throttle 指令参数仅支持传入纯数字",
type: "warning",
});
return;
}
el.removeEventListener("click", el.handler);
el.handler = () => {
el.classList.add("is-disabled");
el.disabled = true;
if (el.disabled) {
binding.value();
setTimeout(() => {
el.classList.remove("is-disabled");
el.disabled = false;
}, binding.arg);
}
};
el.addEventListener("click", el.handler);
},
unmounted(el) {
el.removeEventListener("click", el.handler);
},
};

36
src/hooks/index.js Normal file
View File

@@ -0,0 +1,36 @@
// 样式相关
import useCssVar from "./useCssVar.js";
import { useHexToRgb, useRgbToHex, useDarkColor, useLightColor } from "./useConvertColor.js";
// 组件相关
import useComponentName from "./useComponentName.js";
// 工具相关
import useDeepClone from "./useDeepClone.js";
import useDebounce from "./useDebounce.js";
import useThrottle from "./useThrottle.js";
import useKeyStroke from "./useKeyStroke.js";
import useDataType from "./useDataType.js";
import { useSessionStorage, useLocalStorage } from "./useStorage.js";
import { useRepairZero, useDateFormat } from "./useDateFormat.js";
import { usePureNum, usePhoneNum, useEmailNum, useEmptyObj } from "./useVerification.js";
export default {
useCssVar,
useHexToRgb,
useRgbToHex,
useDarkColor,
useLightColor,
useComponentName,
useDeepClone,
useDebounce,
useThrottle,
useKeyStroke,
useDataType,
useSessionStorage,
useLocalStorage,
useRepairZero,
useDateFormat,
usePureNum,
usePhoneNum,
useEmailNum,
useEmptyObj,
};

View File

@@ -0,0 +1,14 @@
/**
* @description: 获取组件的名称
* @param {Object} route: 路由实例
* @return {String} 组件名称
*/
function useComponentName(route) {
let currentMatched = route.matched;
let currentComponent = currentMatched[currentMatched.length - 1]?.components?.default;
let componentName = currentComponent.name || currentComponent.__name;
// 在 3.2.34 或以上的版本中,使用 <script setup> 的单文件组件会自动根据文件名生成对应的 name 选项,即使是在配合 <KeepAlive> 使用时也无需再手动声明。
// 如果组件内的 <script setup> 没有内容,并且没有自定义组件 name就会返回 undefined
return componentName;
}
export default useComponentName;

View File

@@ -0,0 +1,81 @@
import { ElMessage } from "element-plus";
/**
* 颜色转换函数
* @method useHexToRgb: hex 颜色转 rgb 颜色
* @method useRgbToHex: rgb 颜色转 Hex 颜色
* @method useDarkColor: 加深颜色值
* @method useLightColor: 变浅颜色值
*/
/**
* @description: hex 颜色转 rgb 颜色
* @param {String} str: str 颜色值字符串
* @return {String} hex 值
*/
function useHexToRgb(str) {
let hexs = "";
let reg = /^\#?[0-9A-Fa-f]{6}$/;
if (!reg.test(str)) {
ElMessage.warning("输入错误的hex");
return "";
}
str = str.replace("#", "");
hexs = str.match(/../g);
for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16);
return hexs;
}
/**
* @description: rgb 颜色转 Hex 颜色
* @param {String} r: 代表红色
* @param {String} g: 代表绿色
* @param {String} b: 代表蓝色
* @return {String} Hex 颜色值
*/
function useRgbToHex(r, g, b) {
let reg = /^\d{1,3}$/;
if (!reg.test(r) || !reg.test(g) || !reg.test(b)) {
ElMessage.warning("输入错误的rgb颜色值");
return "";
}
let hexs = [r.toString(16), g.toString(16), b.toString(16)];
for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`;
return `#${hexs.join("")}`;
}
/**
* @description: 加深颜色值
* @param {String} color: 颜色值字符串
* @param {String} level: level 加深的程度限0-1之间
* @return {String} 加深后的颜色
*/
function useDarkColor(color, level) {
let reg = /^\#?[0-9A-Fa-f]{6}$/;
if (!reg.test(color)) {
ElMessage.warning("输入错误的hex颜色值");
return "";
}
let rgb = useHexToRgb(color);
for (let i = 0; i < 3; i++) rgb[i] = Math.floor(rgb[i] * (1 - level));
return useRgbToHex(rgb[0], rgb[1], rgb[2]);
}
/**
* @description: 变浅颜色值
* @param {String} color: 颜色值字符串
* @param {String} level: level 变浅的程度,限 0-1 之间
* @return {String} 变浅后的颜色
*/
function useLightColor(color, level) {
let reg = /^\#?[0-9A-Fa-f]{6}$/;
if (!reg.test(color)) {
ElMessage.warning("输入错误的hex颜色值");
return "";
}
let rgb = useHexToRgb(color);
for (let i = 0; i < 3; i++) rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i]);
return useRgbToHex(rgb[0], rgb[1], rgb[2]);
}
export { useHexToRgb, useRgbToHex, useDarkColor, useLightColor };

8
src/hooks/useCssVar.js Normal file
View File

@@ -0,0 +1,8 @@
/**
* @description: 设置 css 变量
* @param {String} key: 要设置变量
* @param {String} value: 设置的值
*/
export default function useCssVar(key, value) {
document.documentElement.style.setProperty(key, value);
}

60
src/hooks/useDataType.js Normal file
View File

@@ -0,0 +1,60 @@
/**
* @description: 检测数据类型 ( 当只传了 data 时判断 data 的类型, 返回 data 的类型. 当传了 data 和 type 时, 判断 data 和 type 是否是同一类型, 返回布尔值 )
* @param {Any} data: 数据
* @param {Type} type: 类型: 可选择, 如果传了 type 会返回一个布尔值表示 data 是否与 type 类型相等
* @return {String} 数据类型 / 布尔值
*/
function useDataType(data, type = "") {
if (type === "") {
switch (Object.prototype.toString.call(data)) {
case "[object String]":
return "string";
case "[object Number]":
return "number";
case "[object Boolean]":
return "boolean";
case "[object Null]":
return "null";
case "[object Undefined]":
return "undefined";
case "[object Array]":
return "array";
case "[object Function]":
return "function";
case "[object AsyncFunction]":
return "asyncFunction";
case "[object Object]":
return "object";
case "[object Symbol]":
return "symbol";
default:
return "未知类型";
}
} else {
switch (Object.prototype.toString.call(data)) {
case "[object String]":
return type === "string";
case "[object Number]":
return type === "number";
case "[object Boolean]":
return type === "boolean";
case "[object Null]":
return type === "null";
case "[object Undefined]":
return type === "undefined";
case "[object Array]":
return type === "array";
case "[object Function]":
return type === "function";
case "[object AsyncFunction]":
return type === "asyncFunction";
case "[object Object]":
return type === "object";
case "[object Symbol]":
return type === "symbol";
default:
return false;
}
}
}
export default useDataType;

View File

@@ -0,0 +1,57 @@
import { ElMessage } from "element-plus";
import useDataType from "./useDataType.js";
import { usePureNum } from "./useVerification";
/**
* @description: 获得时间格式的枚举
* @return {*}
*/
function getFormatEnum(newDate) {
return {
key: ["YYYY", "YY", "mm", "dd", "HH", "MM", "ss", "m", "d", "H", "M", "s"],
value: {
YYYY: newDate.getFullYear(),
YY: newDate.getYear(),
mm: useRepairZero(newDate.getMonth() - 1),
m: newDate.getMonth() - 1,
dd: useRepairZero(newDate.getDate()),
d: newDate.getDate(),
HH: useRepairZero(newDate.getHours()),
H: newDate.getHours(),
MM: useRepairZero(newDate.getMinutes()),
M: newDate.getMinutes(),
ss: useRepairZero(newDate.getSeconds()),
s: newDate.getSeconds(),
},
};
}
/**
* @description: 补零
* @param {Number} number: 当前数
* @return {Number} 补零后的数
*/
export function useRepairZero(number) {
return number >= 10 ? number : `0${number}`;
}
/**
* @description: 该函数必须传入第一个参数,第二个参数是可选的,函数返回一个格式化好的时间。
* @param {String} date: 任何合法的时间格式、秒或毫秒的时间戳
* @param {String} format: 时间格式可选。默认为YYYY-mm-dd年为"YYYY",月为"mm",日为"dd",时为"hh",分为"MM",秒为"ss",格式可以自由搭配,如: YYYY:mm:ddYYYY-mm-ddYYYY年mm月dd日YYYY年mm月dd日 hh时MM分ss秒YYYY/mm/dd/MM:ss等组合
* @return {String} 格式好的日期
*/
export function useDateFormat(date, format) {
let newDate = "",
time = format || "YYYY-mm-dd";
if (["string", "number"].includes(useDataType(date)) && usePureNum(date) && date.toString().length !== 13) {
newDate = new Date(date * 1000);
} else {
newDate = new Date(date);
}
const formatEnum = getFormatEnum(newDate);
formatEnum.key.forEach((i) => {
time = time.replace(i, formatEnum.value[i]);
});
return time;
}

16
src/hooks/useDebounce.js Normal file
View File

@@ -0,0 +1,16 @@
/**
* @description: 函数防抖
* @param {Function} fn: 函数
* @param {Number} time: 时间
* @return {Function} 处理后的函数
*/
function useDebounce(fn, time = 1000) {
let timeLock = null;
return function (...args) {
clearTimeout(timeLock);
timeLock = setTimeout(() => {
fn(...args);
}, +time);
};
}
export default useDebounce;

31
src/hooks/useDeepClone.js Normal file
View File

@@ -0,0 +1,31 @@
/**
* @description: 判断 arr 是否为一个数组,返回一个 bool 值
* @param {null} arr: 要判断的值
* @return {Boolean} 是否为数组的 Boolean 值
*/
function isArray(arr) {
return Object.prototype.toString.call(arr) === "[object Array]";
}
/**
* @description: 深度克隆
* @param {Object} obj: 要克隆的对象
* @return {Object} 克隆好的对象
*/
function deepClone(obj) {
// 对常见的 “非” 值,直接返回原来值
if ([null, undefined, NaN, false].includes(obj)) return obj;
if (typeof obj !== "object" && typeof obj !== "function") {
// 原始类型直接返回
return obj;
}
var o = isArray(obj) ? [] : {};
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i];
}
}
return o;
}
export default deepClone;

46
src/hooks/useKeyStroke.js Normal file
View File

@@ -0,0 +1,46 @@
import { onMounted, onBeforeUnmount } from "vue";
import { ElMessage } from "element-plus";
import useDataType from "./useDataType.js";
const keys = ["Esc", "Tab", "BackSpace", "Enter", "Shift", "Ctrl", "Alt", "Up", "Down", "Left", "Right"];
const keysCode = [27, 9, 8, 13, 16, 17, 18, 38, 40, 37, 39];
let callback = "";
let keyVal = "";
function keydownFun(e) {
if (e.keyCode == keysCode[keys.indexOf(keyVal)]) {
callback();
}
}
/**
* @description: 监听键盘某些键被按下
* @param {String} key: 要监听的键
* @param {Function} fun: 回调函数
*/
function useKeyStroke(key, fun) {
onMounted(() => {
if (!keys.includes(key)) {
ElMessage({
message: `请输入如下支持的键 Esc、Tab、BackSpace、Enter、Shift、Ctrl、Alt、Up、Down、Left、Right`,
type: "warning",
});
} else if (!fun) {
ElMessage({
message: "回调函数不可为空",
type: "warning",
});
} else if (!["asyncFunction", "function"].includes(useDataType(fun))) {
ElMessage({
message: "请传入正确的回调函数",
type: "warning",
});
} else {
keyVal = key;
callback = fun;
window.addEventListener("keydown", keydownFun);
}
});
onBeforeUnmount(() => {
window.removeEventListener("keydown", keydownFun);
});
}
export default useKeyStroke;

50
src/hooks/useStorage.js Normal file
View File

@@ -0,0 +1,50 @@
import Cookies from "js-cookie";
/**
* window.sessionStorage 浏览器临时缓存
* @method set 设置临时缓存
* @method get 获取临时缓存
* @method remove 移除临时缓存
* @method clear 移除全部临时缓存
*/
export const useSessionStorage = {
set(key, value) {
if (key === "token") return Cookies.set(key, value);
window.sessionStorage.setItem(key, JSON.stringify(value));
},
get(key) {
if (key === "token") return Cookies.get(key);
let json = window.sessionStorage.getItem(key);
return JSON.parse(json);
},
remove(key) {
if (key === "token") return Cookies.remove(key);
window.sessionStorage.removeItem(key);
},
clear() {
Cookies.remove("token");
window.sessionStorage.clear();
},
};
/**
* window.localStorage 浏览器永久缓存
* @method set 设置永久缓存
* @method get 获取永久缓存
* @method remove 移除永久缓存
* @method clear 移除全部永久缓存
*/
export const useLocalStorage = {
set(key, value) {
window.localStorage.setItem(key, JSON.stringify(value));
},
get(key) {
let json = window.localStorage.getItem(key);
return JSON.parse(json);
},
remove(key) {
window.localStorage.removeItem(key);
},
clear() {
window.localStorage.clear();
},
};

19
src/hooks/useThrottle.js Normal file
View File

@@ -0,0 +1,19 @@
/**
* @description: 函数节流
* @param {Function} fn: 函数
* @param {Number} time: 节流时间
* @return {Function} 节流的函数
*/
function useThrottle(fn, time = 1000) {
let flag = true;
return function (...args) {
if (flag) {
flag = false;
fn(...args);
setTimeout(() => {
flag = true;
}, time);
}
};
}
export default useThrottle;

View File

@@ -0,0 +1,69 @@
import { ElMessage } from "element-plus";
import useDataType from "./useDataType.js";
/**
* @description:是否是纯数字
* @param {String|Number} str: 要验证的字符串或数字
* @return {Boolean} 是或否
*/
export function usePureNum(str) {
if (["string", "number"].includes(useDataType(str))) {
var reg = /^\d+$/;
return reg.test(str);
} else {
ElMessage({
message: "仅支持字符串与数字",
type: "warning",
});
}
}
/**
* @description:是否是手机号
* @param {String|Number} str: 要验证的字符串或数字
* @return {Boolean} 是或否
*/
export function usePhoneNum(str) {
if (["string", "number"].includes(useDataType(str))) {
var reg = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/;
return reg.test(str);
} else {
ElMessage({
message: "仅支持字符串与数字",
type: "warning",
});
}
}
/**
* @description: 是否是邮箱
* @param {String} str: 要验证的字符串
* @return {Boolean} 是或否
*/
export function useEmailNum(str) {
if (["string"].includes(useDataType(str))) {
var reg = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;
return reg.test(str);
} else {
ElMessage({
message: "仅支持字符串",
type: "warning",
});
}
}
/**
* @description: 是否是空对象
* @param {Object} obj: 要验证的对象
* @return {Boolean} 是或否
*/
export function useEmptyObj(obj) {
if (["object"].includes(useDataType(obj))) {
return Reflect.ownKeys(obj).length === 0;
} else {
ElMessage({
message: "仅支持对象",
type: "warning",
});
}
}

34
src/main.js Normal file
View File

@@ -0,0 +1,34 @@
import { createApp } from "vue";
import initComponents from "@/utils/components.js";
import initDirective from "@/directive/index.js";
import "./style/normalize.css";
import "./style/public.css";
import router from "./router/index.js";
import pinia from "./store/store";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import locale from 'element-plus/lib/locale/lang/zh-cn'
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
import countTo from 'vue3-count-to';
import App from "./App.vue";
const app = createApp(App);
app.use(pinia);
app.use(router);
app.use(ElementPlus, { locale });
app.use(initComponents);
app.use(initDirective);
app.use(countTo);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.mount("#app");

83
src/router/frontEnd.js Normal file
View File

@@ -0,0 +1,83 @@
import router from "./index.js";
import { staticRoutes, asyncRoutes } from "@/router/routes.js";
import { useRoutes } from "@/store/routes.js";
import { useUser } from "@/store/user.js";
/**
* @description: 初始化路由
* @param {Array} roles: 用户权限
*/
export async function initFrontEndRoutes() {
const storeUser = useUser();
// 触发初始化用户信息
await storeUser.setUserInfo();
const roles = [storeUser.userInfo.userType]; // 权限
// if (roles?.length <= 0) {}; 无权限时的处理
let addRoutes;
if (roles.includes("MG")) {
// 管理员权限包含所有的路由权限
addRoutes = asyncRoutes || [];
} else {
addRoutes = filterAsyncRoutes(asyncRoutes, roles);
}
setAddRoute(addRoutes);
const storeRoutes = useRoutes();
storeRoutes.setRoutesList([...staticRoutes[0].children, ...addRoutes]);
storeRoutes.setAllRoutes(router.getRoutes());
}
/**
* @description: 通过递归过滤异步路由表
* @param {Array} routes: 异步路由
* @param {Array} roles: 用户的所有权限
* @return {Array} 符合权限的异步路由
*/
function filterAsyncRoutes(routes, roles) {
const res = [];
routes.forEach((route) => {
if (hasPermission(route, roles)) {
if (route.children) {
route.children = filterAsyncRoutes(route.children, roles);
}
res.push(route);
}
});
return res;
}
/**
* @description: 判断路由 `meta.roles` 中是否包含当前登录用户权限字段
* @param {Array} route: 路由
* @param {Array} roles: 用户的所有权限
* @return {Boolean} 是否有权限
*/
function hasPermission(route, roles) {
if (route.meta && route.meta.roles) {
return roles.some((role) => route.meta.roles.includes(role));
} else {
return true;
}
}
/**
* @description: 添加动态路由
* @param {Array} addRoutes: 要添加的路由列表
*/
function setAddRoute(addRoutes) {
addRoutes.forEach((route) => {
// 添加嵌套路由
router.addRoute("/", route);
});
}
/**
* @description: 重置路由
* @param {Array} roles: 用户权限
*/
export function resetFrontEndRoute(roles) {
// 获取用户可以访问的权限路由后
const routes = filterAsyncRoutes(asyncRoutes, roles);
// 通过重新添加路由来覆盖
setAddRoute(routes);
}

48
src/router/index.js Normal file
View File

@@ -0,0 +1,48 @@
import { createRouter, createWebHistory } from "vue-router";
import { notFoundAndNoPower, fullScreenRouting, staticRoutes } from "./routes.js";
import { initFrontEndRoutes } from "./frontEnd.js";
import _hook from "@/hooks/index.js";
import { useRoutes } from "@/store/routes.js";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
const router = createRouter({
history: createWebHistory(),
routes: [...notFoundAndNoPower, ...fullScreenRouting, ...staticRoutes],
});
NProgress.configure({ showSpinner: false });
router.beforeEach(async (to, from, next) => {
NProgress.start();
const token = _hook.useLocalStorage.get("token");
if (to.path === "/login" && !token) {
next();
NProgress.done();
} else {
if (!token) {
next("/login");
_hook.useLocalStorage.clear();
} else if (token && to.path === "/login") {
next("/home");
NProgress.done();
} else {
const storeRoutes = useRoutes();
if (storeRoutes.routesList.length === 0) {
await initFrontEndRoutes(); // 初始化前端路由
storeRoutes.setNavbar("/home"); // 每次初始化时都添加首页
next({ path: to.path, query: to.query });
NProgress.done();
} else {
storeRoutes.setNavbar(to.path);
next();
NProgress.done();
}
}
}
});
router.afterEach((to, from, next) => {
NProgress.done();
});
export default router;

524
src/router/routes.js Normal file
View File

@@ -0,0 +1,524 @@
import bridge from "@/views/layout/bridge.vue"; // 多级路由
import layout from "@/views/layout/layout.vue" // 只需要展示一页
/**
* 路由参数说明
*
* path: !!! 路由 path 路径需完整,因为菜单中所有路由跳转均使用 path 跳转。
* 如:
* 父路由 path:'/home'
* 子路由 path:'/home/page'
* 孙路由 path:'/home/page/index'
* 首页 path 必须为 '/home'
* path 路径作为菜单唯一标识不可重复, 且在没有配置 meta.title 时使用 path 作为标题.
* meta: {
* title: 菜单的标题 ( 缺少 title 时会使用路由的 path 作为标题)
* isHide: 是否隐藏此路由
* isKeepAlive: 是否缓存路由
* roles: 当前路由权限标识,取角色管理。控制路由显示、隐藏。
* icon: 图标 elementUI
* activeMenu: 需要保持高亮的菜单
* }
*/
/**
* 定义404、401界面
*/
export const notFoundAndNoPower = [
{
path: "/:pathMatch(.*)*",
name: "notFound",
component: () => import("@/views/error/404.vue"),
meta: {
title: "404",
isHide: true,
},
},
{
path: "/401",
name: "noPower",
component: () => import("@/views/error/401.vue"),
meta: {
title: "401",
isHide: true,
},
},
];
/**
* 全屏路由页面
* 此处的页面会占满浏览器的整个屏幕
*/
export const fullScreenRouting = [
{
path: "/login",
name: "login",
component: () => import("@/views/login/login.vue"),
meta: {
title: "登录",
isHide: true,
},
},
];
/**
* 静态路由
* 没有权限要求的基本页面
* 所有角色都可以访问
*/
export const staticRoutes = [
{
path: "/",
name: "/",
component: layout,
redirect: "/home",
meta: {
isHide: true,
},
children: [
{
path: "/home",
component: () => import("@/views/home.vue"),
meta: {
title: "首页",
icon: "House",
// isKeepAlive: true,
},
},
// {
// path: "/makes",
// component: bridge,
// redirect: "/makes/svgIcon",
// meta: {
// title: "组件封装",
// icon: "Edit",
// },
// children: [
// {
// path: "/makes/svgIcon",
// component: () => import("@/views/makes/svgIcon.vue"),
// meta: {
// title: "SvgIcon",
// },
// },
// ],
// },
// {
// path: "/tools",
// component: bridge,
// redirect: "/tools/editor",
// meta: {
// title: "工具",
// icon: "Flag",
// },
// children: [
// {
// path: "/tools/editor",
// component: () => import("@/views/tools/editor.vue"),
// meta: {
// title: "编辑器",
// icon: "Edit",
// },
// },
// ],
// },
// {
// path: "/other",
// component: bridge,
// redirect: "/other/watermark",
// meta: {
// title: "其他",
// icon: "Box",
// },
// children: [
// {
// path: "/other/watermark",
// component: () => import("@/views/other/watermark.vue"),
// meta: {
// title: "水印",
// },
// },
// {
// path: "/other/resize",
// component: () => import("@/views/other/resize.vue"),
// meta: {
// title: "监控元素尺寸变化",
// },
// },
// ],
// },
],
},
];
/**
* 异步路由
* 需要根据用户角色动态加载的路由
*/
export const asyncRoutes = [
// {
// path: "/demo",
// component: bridge,
// redirect: "/demo/demo1/demo11",
// meta: {
// title: "权限路由",
// icon: "Edit",
// },
// children: [
// {
// path: "demo1",
// component: bridge,
// meta: {
// title: "用户1路由-1",
// icon: "Edit",
// roles: ["yonghu1"],
// },
// children: [
// {
// path: "/demo/demo1/demo11",
// component: () => import("@/views/demo/demo11.vue"),
// name: "demo11",
// meta: {
// title: "用户1路由-11",
// },
// },
// {
// path: "/demo/demo1/demo12",
// component: () => import("@/views/demo/demo12.vue"),
// name: "demo12",
// meta: {
// title: "用户1路由-12",
// },
// children: [
// {
// path: "/demo/demo1/demo121",
// component: () => import("@/views/demo/demo121.vue"),
// name: "demo121",
// meta: {
// title: "用户1路由-121",
// },
// },
// {
// path: "/demo/demo1/demo122",
// component: () => import("@/views/demo/demo122.vue"),
// name: "demo122",
// meta: {
// title: "用户1路由-122",
// },
// },
// ],
// },
// {
// path: "/demo/demo1/demo13",
// component: () => import("@/views/demo/demo13.vue"),
// name: "demo13",
// meta: {
// title: "用户1路由-13",
// },
// },
// ],
// },
// {
// path: "/demo/demo2",
// component: bridge,
// meta: {
// title: "用户2路由-2",
// icon: "Edit",
// roles: ["yonghu2"],
// },
// children: [
// {
// path: "/demo/demo2/demo21",
// component: () => import("@/views/demo/demo21.vue"),
// meta: {
// title: "用户2路由-21",
// icon: "Edit",
// },
// },
// {
// path: "/demo/demo2/demo22",
// component: () => import("@/views/demo/demo22.vue"),
// meta: {
// title: "用户2路由-22",
// icon: "Edit",
// },
// },
// {
// path: "/demo/demo2/demo23",
// component: () => import("@/views/demo/demo23.vue"),
// meta: {
// title: "用户2路由-23",
// icon: "Edit",
// },
// },
// ],
// },
// ],
// },
// {
// path: '/system',
// component: bridge,
// redirect: '/system/menu_manage',
// meta: {
// title: '系统管理',
// icon: 'Setting',
// roles: ['FO']
// },
// children: [
// {
// path: '/system/menu_manage',
// component: () => import('@/views/system/menuMange.vue'),
// meta: {
// title: '菜单管理',
// icon: 'Stopwatch'
// }
// }
// ]
// },
{
path: '/organization',
component: layout,
meta: {
title: '大机构管理',
roles: ['MG'],
isHide: true
},
redirect: '/organization/big_organization',
children: [
{
path: '/organization/big_organization',
component: () => import('@/views/organization/big_organization.vue'),
meta: {
title: '大机构',
icon: 'Tickets'
}
}
]
},
{
path: '/mini_organization_manage',
component: layout,
meta: {
title: '小机构管理',
roles: ['FO'],
isHide: true
},
redirect: '/mini_organization_manage/mini_organization',
children: [
{
path: '/mini_organization/mini_organization',
component: () => import('@/views/organization/mini_organization.vue'),
meta: {
title: '小机构',
icon: 'SetUp'
}
}
]
},
{
path: '/agent_manage',
component: layout,
meta: {
title: '大代理管理',
roles: ['FO', 'SO'],
isHide: true
},
redirect: '/agent_manage/agent_list',
children: [
{
path: '/agent_manage/agent_list',
component: () => import('@/views/organization/agent_list.vue'),
meta: {
title: '大代理',
icon: 'Discount'
}
}
]
},
{
path: '/promotion_manage',
component: layout,
meta: {
title: '代理',
icon: 'Connection',
},
redirect: '/promotion_manage/one_promotion_list',
children: [
{
path: '/promotion_manage/one_promotion_list',
component: () => import('@/views/organization/one_promotion_list.vue'),
meta: {
title: '一级代理',
icon: 'User',
roles: ['FO', 'SO', 'AG']
}
},
{
path: '/promotion_manage/two_promotion_list',
component: () => import('@/views/organization/two_promotion_list.vue'),
meta: {
title: '二级代理',
icon: 'User',
roles: ['FO', 'SO', 'AG', 'FB']
}
}
]
},
{
path: '/shop_manage',
component: layout,
meta: {
title: '商家管理',
roles: ['FO', 'SO', 'AG', 'FB', 'SB'],
icon: 'Handbag'
},
redirect: '/shop_manage/shop_list',
children: [
{
path: '/shop_manage/shop_list',
name: 'shop_list',
component: () => import('@/views/organization/shop_list.vue'),
meta: {
title: '商家列表',
icon: 'Tickets'
}
},
{
path: '/shop_manage/shop_detail',
name: 'shop_detail',
component: () => import('@/views/organization/shop_detail.vue'),
meta: {
title: '详情',
isHide: true,
activeMenu: '/shop_manage/shop_list'
}
},
{
path: '/shop_manage/maker_apply',
component: () => import('@/views/organization/maker_apply.vue'),
meta: {
title: '创客申请',
icon: 'User'
}
}
]
},
{
path: '/withdraw_manage',
component: layout,
meta: {
title: '提现管理',
roles: ['FO', 'SO'],
isHide: true
},
redirect: '/withdraw_manage/withdraw_list',
children: [
{
path: '/withdraw_manage/withdraw_list',
component: () => import('@/views/withdraw/withdraw_list.vue'),
meta: {
title: '提现申请',
icon: 'CreditCard'
}
}
]
},
{
path: '/device_manage',
component: layout,
meta: {
title: '设备管理',
isHide: true
},
redirect: '/device_manage/device_list',
children: [
{
path: '/device_manage/device_list',
component: () => import('@/views/device/device_list.vue'),
meta: {
title: '设备列表',
icon: 'TakeawayBox'
}
}
]
},
{
path: '/total_earnings',
component: layout,
meta: {
title: '累计收益管理',
isHide: true
},
redirect: '/total_earnings/total_earnings_list',
children: [
{
path: '/total_earnings/total_earnings_list',
component: () => import('@/views/total_earnings/total_earnings_list.vue'),
meta: {
title: '累计收益',
icon: 'Coin'
}
}
]
},
{
path: '/promotion',
component: layout,
meta: {
title: '推广图片管理',
isHide: true,
roles: ['MG']
},
redirect: '/promotion/promotion_list',
children: [
{
path: '/promotion/promotion_list',
component: () => import('@/views/promotion/promotion_list.vue'),
meta: {
title: '推广图片',
icon: 'PictureRounded'
}
}
]
},
{
path: '/app_manage',
component: layout,
meta: {
title: 'APP管理',
icon: 'Iphone',
roles: ['MG']
},
redirect: '/app_manage/menu_list',
children: [
{
path: '/app_manage/menu_list',
component: () => import('@/views/app_manage/menu_list.vue'),
meta: {
title: '菜单管理',
icon: 'Tickets'
}
}
]
},
{
path: '/setting',
component: layout,
meta: {
title: '系统设置',
icon: 'Setting',
roles: ['MG']
},
redirect: '/setting/appid_manage',
children: [
{
path: '/setting/appid_manage',
component: () => import('@/views/setting/appid_manage.vue'),
meta: {
title: 'Appid管理',
icon: 'Tickets'
}
}
]
}
];

50
src/store/configure.js Normal file
View File

@@ -0,0 +1,50 @@
import { defineStore } from "pinia";
import { ENUMS } from "@/utils/enums.js";
export const useConfigure = defineStore("useConfigure", {
state: () => {
return {
projectName: '银收客机构管理端',
defaultActive: "/home", // 默认选中的菜单
configure: {
// ---------- 主题 ----------
themeColor: "#3186FD", // 主题色
// ---------- 菜单 ----------
collapse: false, // 是否收起侧边栏
menuMode: ENUMS.layoutModeEnum.key[0], // 菜单的布局模式
menuBGColor: "#ffffff", // 菜单的背景色
textColor: "#333333", // 菜单的文字颜色
activeTextColor: "#ffffff", // 菜单的激活文字颜色
showMenuLogo: true, // 菜单的 logo
// ---------- 分栏 ----------
columnBgColor: "#3186FD",
// ---------- 导航栏 ----------
navbarMode: ENUMS.navbarModeEnum.key[1],
navbarIcon: false,
// 过度
componentTransition: true,
componentTransitionMode: ENUMS.componentTransitionEnum.key[1],
},
};
},
getters: {},
actions: {
/**
* @description: 初始化配置
* @param {Object} configure: 配置
*/
initConfigure(configure) {
const keys = Object.keys(configure);
keys.forEach((key) => {
this.configure[key] = configure[key];
});
},
// 设置默认激活的路由
setDefaultActive(value) {
this.defaultActive = value;
},
change(key, value) {
this.configure[key] = value;
},
},
});

128
src/store/routes.js Normal file
View File

@@ -0,0 +1,128 @@
import { defineStore } from "pinia";
import { useConfigure } from "./configure.js";
export const useRoutes = defineStore("useRoutes", {
state: () => {
return {
routesList: [],
allRoutes: {},
breadcrumb: [],
breadcrumbKeys: [],
navList: new Map(),
activeRoute: "",
cachedRoute: [],
};
},
actions: {
/**
* @description: 设置路由列表
* @param {Array} routes: 路由列表
*/
setRoutesList(routes) {
this.routesList = routes;
},
/**
* @description: 设置当前的所有路由
* @param {Array} routes: 所有路由
*/
setAllRoutes(routes) {
routes.forEach((i) => {
this.allRoutes[i.path] = i;
});
},
/**
* @description: 设置需要缓存的路由
* @param {String} name: 要缓存的路由组件名称
*/
setCachedRoute(name) {
if (!this.cachedRoute.includes(name)) {
this.cachedRoute.push(name);
}
},
/**
* @description: 设置面包屑
* @param {Array} matched: 相配的路由
*/
setBreadcrumb(matched) {
this.breadcrumb = matched;
this.breadcrumbKeys = [];
matched.forEach((i) => {
this.breadcrumbKeys.push(i.path);
});
},
/**
* @description: 设置导航栏
* @param {String} route: 路由
*/
setNavbar(route) {
if (this.allRoutes[route]) {
this.navList.set(route, this.allRoutes[route]);
this.activeRoute = route;
useConfigure().setDefaultActive(route);
}
},
/**
* @description: 设置 navList 的列表
* @param {String} type: 操作类型
*/
setNavList(type) {
return new Promise((resolve) => {
const navListKeys = [...this.navList].slice(1);
switch (type) {
case "else":
for (let i = 0; i < navListKeys.length; i++) {
if (navListKeys[i][0] !== this.activeRoute) {
this.navList.delete(navListKeys[i][0]);
}
}
break;
case "left":
for (let i = 0; i < navListKeys.length; i++) {
if (navListKeys[i][0] == this.activeRoute) return;
this.navList.delete(navListKeys[i][0]);
}
break;
case "right":
for (let i = navListKeys.length - 1; i >= 0; i--) {
if (navListKeys[i][0] == this.activeRoute) return;
this.navList.delete(navListKeys[i][0]);
}
break;
case "all":
for (let i = 0; i < navListKeys.length; i++) {
this.navList.delete(navListKeys[i][0]);
}
resolve();
break;
}
});
},
/**
* @description: 移除 navbar 中的某一项
* @param {String} route: 要移除的路由项
*/
deleteNavItem(route) {
return new Promise((resolve) => {
if (route == this.activeRoute) {
let navList = [...this.navList],
length = navList.length,
r = "";
for (let i = 0; i < length; i++) {
if (navList[i][0] == route) {
if (i == length - 1) {
r = navList[i - 1];
} else if (i < length - 1) {
r = navList[i + 1];
}
this.navList.delete(route);
resolve(r);
return;
}
}
} else {
this.navList.delete(route);
}
});
},
},
});

3
src/store/store.js Normal file
View File

@@ -0,0 +1,3 @@
import { createPinia } from "pinia";
const pinia = createPinia();
export default pinia;

44
src/store/user.js Normal file
View File

@@ -0,0 +1,44 @@
import { defineStore } from "pinia";
import _hook from "@/hooks/index.js";
import { login, getUserInfo } from "@/api/user.js";
export const useUser = defineStore("useUser", {
state: () => {
return {
userInfo: {},
token: _hook.useLocalStorage.get("token") || "",
};
},
getters: {},
actions: {
// 设置用户信息
async setUserInfo() {
if (_hook.useLocalStorage.get("userInfo")) {
this.userInfo = _hook.useLocalStorage.get("userInfo");
} else {
this.userInfo = await this.getUserInfo();
}
},
/**
* @description: 用户登录
* @param {*} param: 登录的参数
*/
userlogin(param) {
return login(param).then((res) => {
// console.log(res);
this.userInfo = res;
_hook.useLocalStorage.set("token", this.userInfo.token);
_hook.useLocalStorage.set("userInfo", this.userInfo.user);
return this.userInfo;
});
},
/**
* @description: 获取用户信息
*/
getUserInfo() {
return getUserInfo().then((res) => {
return res.userInfo;
});
},
},
});

379
src/style/normalize.css vendored Normal file
View File

@@ -0,0 +1,379 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body,
html,
ul,
ol,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
p,
form,
fieldset,
legend,
input,
textarea,
select,
button,
th,
td {
padding: 0;
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box;
/* 1 */
height: 0;
/* 1 */
overflow: visible;
/* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
a,
a:focus,
a:hover,
a:active {
cursor: pointer;
color: inherit;
text-decoration: none;
outline: none;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none;
/* 1 */
text-decoration: underline;
/* 2 */
text-decoration: underline dotted;
/* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
line-height: 1.15;
/* 1 */
margin: 0;
/* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box;
/* 1 */
color: inherit;
/* 2 */
display: table;
/* 1 */
max-width: 100%;
/* 1 */
padding: 0;
/* 3 */
white-space: normal;
/* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}
div,
p {
box-sizing: border-box;
}
:focus {
outline: 0 !important;
}

198
src/style/public.css Normal file
View File

@@ -0,0 +1,198 @@
/* 定位 */
.relative {
position: relative;
}
.absolute {
position: absolute;
}
.fixed {
position: fixed;
}
/* 层深 */
.z-1 {
z-index: 10;
}
.z-2 {
z-index: 20;
}
.z-3 {
z-index: 30;
}
.z-4 {
z-index: 40;
}
.z-5 {
z-index: 50;
}
.z-6 {
z-index: 60;
}
.z-7 {
z-index: 70;
}
.z-8 {
z-index: 80;
}
.z-9 {
z-index: 90;
}
.z-10 {
z-index: 100;
}
/* 字体加粗 */
.font-bold {
font-weight: bold;
}
/* 文本对齐 */
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-justify {
text-align: justify;
}
/* 文本溢出省略 */
.text-ellipsis-1 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.text-ellipsis-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.text-ellipsis-3 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
/* flex 布局 */
.flex {
display: flex;
}
.flex-lr {
flex-direction: row;
}
.flex-rl {
flex-direction: row-reverse;
}
.flex-tb {
flex-direction: column;
}
.flex-bt {
flex-direction: column-reverse;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-nowrap {
flex-wrap: nowrap;
}
.justify-start {
justify-content: flex-start;
}
.justify-end {
justify-content: flex-end;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.justify-evenly {
justify-content: space-evenly;
}
.align-start {
align-items: flex-start;
}
.align-end {
align-items: flex-end;
}
.align-center {
align-items: center;
}
.align-baseline {
align-items: baseline;
}
.align-stretch {
align-items: stretch;
}
.flex-1 {
flex: 1 1 0;
}
.flex-auto {
flex: 1 1 auto;
}
.flex-initial {
flex: 0 1 auto;
}
.flex-none {
flex: none;
}
.flex-grow-0 {
flex-grow: 0;
}
.flex-grow {
flex-grow: 1;
}
/* hover 样式 */
.pointer {
cursor: pointer;
}

27
src/style/root.scss Normal file
View File

@@ -0,0 +1,27 @@
:root {
--admin-column-bg-color: #282c34;
}
.scrollbar-y {
&::-webkit-scrollbar {
width: 8px;
background-color: #f1f1f1;
border-radius: 8px;
}
&::-webkit-scrollbar-thumb {
background-color: #dfdfdf;
border-radius: 8px;
}
}
.scrollbar-x {
&::-webkit-scrollbar {
height: 8px;
background-color: #f1f1f1;
border-radius: 8px;
}
&::-webkit-scrollbar-thumb {
background-color: #dfdfdf;
border-radius: 8px;
}
}

8
src/utils/components.js Normal file
View File

@@ -0,0 +1,8 @@
import SvgIcon from "@/components/SvgIcon.vue";
const initComponents = {
install(app) {
app.component("SvgIcon", SvgIcon);
},
};
export default initComponents;

25
src/utils/enums.js Normal file
View File

@@ -0,0 +1,25 @@
export const ENUMS = {
// 布局模式
layoutModeEnum: {
key: ["modeA", "modeB"],
value: ["纵向", "分栏"],
},
// 布局模式
navbarModeEnum: {
key: ["modeA", "modeB", "modeC"],
value: ["圆滑", "卡片", "灵动"],
},
// 设置配置
setColorEnum: {
themeColor: "--el-color-primary",
menuBGColor: "--el-menu-bg-color",
textColor: "--el-menu-text-color",
activeTextColor: "--el-menu-active-color",
columnBgColor: "--admin-column-bg-color",
},
// 组件切换
componentTransitionEnum: {
key: ["mainA", "mainB"],
value: ["下至上", "右至左"],
},
};

95
src/utils/index.js Normal file
View File

@@ -0,0 +1,95 @@
/**
* 校验手机号码
* @param {*} tel
*/
export function validPhone(tel) {
const reg = /^1[3-9]\d{9}$/;
return reg.test(tel);
}
/**
* 机构名称
*/
export const typeNames = {
"AG": '代理',
"FB": '一级业务员',
"FO": '大机构',
"MC": '商家',
"SB": '二级业务员',
"SO": '小机构',
"XW": '小微商户',
"MG": '平台'
}
/**
* 机构列表
*/
export const organizationList = [
{
type_code: 'MG',
type_name: '平台'
},
{
type_code: 'AG',
type_name: '代理'
},
{
type_code: 'FB',
type_name: '一级业务员'
},
{
type_code: 'FO',
type_name: '大机构'
},
{
type_code: 'MC',
type_name: '商家'
},
{
type_code: 'SB',
type_name: '二级业务员'
},
{
type_code: 'SO',
type_name: '小机构'
},
{
type_code: 'XW',
type_name: '小微商户'
}
]
/**
* 添加事对象下一级类型
*/
export const addOrganizations = {
'MG': 'FO',
'FO': 'SO',
'SO': 'AG'
}
/**
* 去除字符串中除了数字和点以外的其他字符
* @param {Object} obj
*/
export function clearNoNum(obj) {
//如果用户第一位输入的是小数点,则重置输入框内容
if (obj.value != '' && obj.value.substr(0, 1) == '.') {
obj.value = '';
}
obj.value = obj.value.replace(/^0*(0\.|[1-9])/, '$1'); //粘贴不生效
obj.value = obj.value.replace(/[^\d.]/g, ''); //清除“数字”和“.”以外的字符
obj.value = obj.value.replace(/\.{2,}/g, '.'); //只保留第一个. 清除多余的
obj.value = obj.value
.replace('.', '$#$')
.replace(/\./g, '')
.replace('$#$', '.');
obj.value = obj.value.replace(/^(\-)*(\d+)\.(\d\d).*$/, '$1$2.$3'); //只能输入两个小数
if (obj.value.indexOf('.') < 0 && obj.value != '') {
//以上已经过滤,此处控制的是如果没有小数点,首位不能为类似于 01、02的金额
if (obj.value.substr(0, 1) == '0' && obj.value.length == 2) {
obj.value = obj.value.substr(1, obj.value.length);
}
}
return obj.value;
}

72
src/utils/request.js Normal file
View File

@@ -0,0 +1,72 @@
import axios from "axios";
import { ElMessage } from "element-plus";
import _hook from "@/hooks/index.js";
import NProgress from "nprogress";
import router from '@/router'
const service = axios.create({
baseURL: import.meta.env.MODE == 'development' ? '/api/' : '/api/admin',
// withCredentials: true, // 跨域请求时发送 cookies
timeout: 5000, // 请求超时
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
NProgress.start();
// 在发送请求之前做些什么 token
if (_hook.useLocalStorage.get("token")) {
// 让每个请求携带 token
// ['X-Token'] 是自定义标题键
// 请根据实际情况修改
config.headers["token"] = _hook.useLocalStorage.get("token");
config.headers["loginName"] = _hook.useLocalStorage.get("userInfo").loginName;
config.headers["userId"] = _hook.useLocalStorage.get("userInfo").userId;
// config.headers['Content-Type'] = 'application/json'
}
return config;
},
(error) => {
NProgress.done();
// 处理请求错误
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(response) => {
NProgress.done();
// 对响应数据做点什么
if (+response.status === 200) {
if (+response.data.code == '000000') {
return response.data.data;
} else if (+response.data.code == '999999') {
ElMessage.error('登录已过期,请重新登录')
_hook.useLocalStorage.clear()
router.replace("/login")
return Promise.reject('登录已过期,请重新登录')
} else {
// 响应错误
ElMessage.error(response.data.message)
return Promise.reject(response.data.message)
}
}
},
(error) => {
NProgress.done();
// 对响应错误做点什么
if (error.message.indexOf("timeout") != -1) {
ElMessage.error("网络超时");
} else if (error.message == "Network Error") {
ElMessage.error("网络连接错误");
} else {
console.log(error);
if (error.response.data) ElMessage.error(error.response.statusText);
else ElMessage.error("接口路径找不到");
}
return Promise.reject(error);
}
);
export default service;

69
src/utils/watermark.js Normal file
View File

@@ -0,0 +1,69 @@
let id = "uniqueIdProhibitsDuplication",
watermarkText = "";
function createWatermark() {
if (document.getElementById(id) !== null) {
document.body.removeChild(document.getElementById(id));
}
// 创建一个画布
const can = document.createElement("canvas");
// 设置画布的长宽
can.width = 250;
can.height = 200;
const cans = can.getContext("2d");
// 旋转角度
cans.rotate((-20 * Math.PI) / 180);
cans.font = "16px Vedana";
// 设置填充绘画的颜色、渐变或者模式
cans.fillStyle = "rgba(200, 200, 200, 0.35)";
// 设置文本内容的当前对齐方式
cans.textAlign = "left";
// 设置在绘制文本时使用的当前文本基线
cans.textBaseline = "Middle";
// 在画布上绘制填色的文本输出的文本开始绘制文本的X坐标位置开始绘制文本的Y坐标位置
cans.fillText(watermarkText, can.width / 8, can.height / 2);
const div = document.createElement("div");
div.id = id;
div.style.pointerEvents = "none"; //禁用鼠标事件
div.style.top = "30px";
div.style.left = "0px";
div.style.position = "fixed";
div.style.zIndex = "999999";
div.style.width = document.documentElement.clientWidth - 20 + "px";
div.style.height = document.documentElement.clientHeight - 20 + "px";
div.style.background = "url(" + can.toDataURL("image/png") + ") left top repeat";
document.body.appendChild(div);
mutationFun();
}
// 添加水印
function set(text = "Vue3 ElePlus Admin") {
watermarkText = text;
createWatermark();
window.addEventListener("resize", createWatermark);
}
// 移除水印
function remove() {
document.body.removeChild(document.getElementById(id));
window.removeEventListener("resize", createWatermark);
}
// 监听元素的变化
function mutationFun() {
const mutation = new MutationObserver((el) => {
mutation.disconnect();
createWatermark();
});
const config = {
attributes: true,
subtree: true,
childList: true,
};
mutation.observe(document.getElementById(id), config);
}
export default {
set,
remove,
};

View File

@@ -0,0 +1,347 @@
<template>
<div class="card">
<el-space>
<el-button type="primary" icon="Plus" @click="addHandle">添加菜单</el-button>
</el-space>
<div class="mt15">
<el-space>
<el-select placeholder="请选择分组" v-model="tableOptions.navcode">
<el-option :value="item.id" :label="item.name" v-for="item in menuGroups" :key="item.id"></el-option>
</el-select>
<el-select placeholder="请选择导航" v-model="tableOptions.navName">
<el-option :value="item.id" :label="item.name" v-for="item in navcodes" :key="item.id"></el-option>
</el-select>
<el-button type="primary" icon="Search" @click="searchHandle">搜索</el-button>
<el-button icon="RefreshRight" @click="resizeTable">重置</el-button>
</el-space>
</div>
<div class="table mt15">
<el-table ref="table" :data="tableOptions.list" border height="100%" v-loading="tableOptions.loading">
<el-table-column prop="id" label="ID" width="50"></el-table-column>
<el-table-column label="图标" width="100">
<template #default="scope">
<el-image style="width: 50px; height: 50px" :src="scope.row.icon" preview-teleported
hide-on-click-modal :preview-src-list="[scope.row.icon]" fit="cover">
</el-image>
</template>
</el-table-column>
<el-table-column prop="menuGroup" label="分组">
<template #default="scope">
<el-text>{{ menuGroups.find(item => item.id == scope.row.menuGroup).name }}</el-text>
</template>
</el-table-column>
<el-table-column prop="name" label="名称"></el-table-column>
<el-table-column prop="navName" label="导航"></el-table-column>
<el-table-column prop="url" label="链接"></el-table-column>
<el-table-column label="是否显示" width="100">
<template #default="scope">
<el-text v-if="scope.row.visible == 0" type="info">不显示</el-text>
<el-text v-if="scope.row.visible == 1" type="primary">显示</el-text>
</template>
</el-table-column>
<el-table-column label="是否小程序" width="100">
<template #default="scope">
<el-text v-if="scope.row.isapplets == 0" type="info"></el-text>
<el-text v-if="scope.row.isapplets == 1" type="primary"></el-text>
</template>
</el-table-column>
<el-table-column label="是否uniapp" width="100">
<template #default="scope">
<el-text v-if="scope.row.isuniapp == 0" type="info"></el-text>
<el-text v-if="scope.row.isuniapp == 1" type="primary"></el-text>
</template>
</el-table-column>
<el-table-column label="显示(安卓/iso)" width="100">
<template #default="scope">
<el-text v-if="scope.row.isandroidenabled == 0" type="info"></el-text>
<el-text v-if="scope.row.isandroidenabled == 1" type="primary"></el-text>
<el-text type="info">/</el-text>
<el-text v-if="scope.row.isiphoneenabled == 0" type="info"></el-text>
<el-text v-if="scope.row.isiphoneenabled == 1" type="primary"></el-text>
</template>
</el-table-column>
<el-table-column prop="username" label="小程序ID"></el-table-column>
<el-table-column prop="path" label="跳转路径/uniApp路径"></el-table-column>
<el-table-column prop="createTime" label="创建时间">
<template #default="scope">
{{ dayjs(scope.row.createTime).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column prop="updateTime" label="更新时间">
<template #default="scope">
{{ dayjs(scope.row.updateTime).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column label="操作" width="140">
<template #default="scope">
<el-button type="primary" size="small" icon="EditPen">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="mt15">
<el-pagination layout="prev, pager, next, total, sizes, jumper" background
v-model:current-page="tableOptions.pageNum" v-model:page-size="tableOptions.pageSzie"
:page-size="tableOptions.pageSzie" :page-sizes="[10, 20, 30, 50]" :total="tableOptions.total"
@size-change="paginationChange" @current-change="paginationChange" />
</div>
</div>
<el-dialog title="增加菜单" v-model="showDialog" @closed="dialogClosed">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100">
<el-form-item prop="name" label="菜单名称">
<el-input placeholder="请输入菜单名称" v-model="form.name" />
</el-form-item>
<el-form-item prop="code" label="菜单code">
<el-input placeholder="请输入菜单code" v-model="form.code" />
</el-form-item>
<el-form-item prop="sort" label="排序">
<el-input placeholder="请输入菜单排序" v-model="form.sort" />
</el-form-item>
<el-form-item prop="url" label="图片url">
<el-upload ref="uploadRef" v-model:file-list="fileList" :limit="1" :on-exceed="handleExceed"
list-type="picture" :auto-upload="false" @change="selectFile" @remove="removeFile">
<template #trigger>
<el-button type="primary" icon="Picture">选择图片</el-button>
</template>
</el-upload>
</el-form-item>
<el-form-item prop="navcode" label="导航">
<el-select v-model="form.navcode">
<el-option :label="item.name" :value="item.id" v-for="item in navcodes" :key="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="menuGroup" label="分组">
<el-select v-model="form.menuGroup">
<el-option :label="item.name" :value="item.id" v-for="item in menuGroups" :key="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="type" label="是否显示">
<el-radio-group v-model="form.visible">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="type" label="安卓是否显示">
<el-radio-group v-model="form.isAndroidEnabled">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="type" label="ios是否显示">
<el-radio-group v-model="form.isIphoneEnabled">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-space>
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" :loading="formLoading" @click="submitHandle">提交</el-button>
</el-space>
</template>
</el-dialog>
</template>
<script setup>
import { dayjs } from 'element-plus';
import { appMenuPage, getDictGroup, uploadOSS, appMenuSave } from '@/api/home.js'
import { onMounted, reactive } from 'vue';
const table = ref(null)
const navcodes = ref([])
const menuGroups = ref([])
// 表格参数
const tableOptions = reactive({
loading: true,
navcode: '',
navName: '',
list: [],
total: 0,
pageNum: 1,
pageSzie: 10
})
// 添加菜单
function addHandle() {
showDialog.value = true
}
// 重置表格
function resizeTable() {
tableOptions.navcode = ''
tableOptions.navName = ''
searchHandle()
}
// 搜索
function searchHandle() {
tableOptions.pageNum = 1;
paginationChange()
}
// 分页回调
function paginationChange() {
tableOptions.loading = true
appMenuPageAjax()
}
// app菜单list
async function appMenuPageAjax() {
try {
const res = await appMenuPage({
page: tableOptions.pageNum,
size: tableOptions.pageSzie
})
tableOptions.loading = false
tableOptions.list = res.list
tableOptions.total = res.total
table.value.setScrollTop(0)
} catch (error) { }
}
// 获取分组字典值
async function getMenuGrounp() {
try {
const res = await getDictGroup('APP_MENU_GROUP')
menuGroups.value = res
} catch (error) { }
}
// 获取导航字典值
async function getNavCodes() {
try {
const res = await getDictGroup('APP_MENU_NAV')
navcodes.value = res
} catch (error) { }
}
// 显示添加表单
const showDialog = ref(false)
const formLoading = ref(false)
const formRef = ref(null)
const fileList = ref([])
// 创建表单
const form = reactive({
name: '', // 菜单名称
code: '', // 菜单code
sort: '', // 排序
url: '', // 图标
navcode: '', // 导航选择
menuGroup: '', // 分组
visible: 1, // 是否显示0否1是
isAndroidEnabled: 1, // 安卓是否显示
isIphoneEnabled: 1, // ios是否显示
})
// 校验是否选择图片
const imgValidate = (rule, value, callback) => {
if (fileList.value.length <= 0) {
return callback(new Error('请选择图片'))
} else {
callback()
}
}
// 表单校验规则
const rules = reactive({
name: [
{
required: true,
message: '',
trigger: 'blur'
}
],
code: [
{
required: true,
message: '',
trigger: 'blur'
}
],
sort: [
{
required: true,
message: '',
trigger: 'blur'
}
],
url: [
{
required: true,
validator: imgValidate,
trigger: 'change'
}
],
navcode: [
{
required: true,
message: '',
trigger: 'change'
}
],
menuGroup: [
{
required: true,
message: '',
trigger: 'change'
}
]
})
// 只能选择一个文件,替换之前的文件
const handleExceed = (files) => {
uploadRef.value.clearFiles()
const file = files[0]
file.uid = genFileId()
uploadRef.value.handleStart(file)
form.url = ''
}
// 选择图片
const selectFile = async (file) => {
fileList.value = [file]
}
// 移除图片
const removeFile = async () => {
fileList.value = []
form.url = ''
}
// 表单关闭
function dialogClosed() {
formRef.value.resetFields()
fileList.value = []
}
// 提交表单
const submitHandle = async () => {
await formRef.value.validate(async (vaild) => {
if (vaild) {
try {
formLoading.value = true
form.url = await uploadOSS(fileList.value[0].raw)
await appMenuSave(form)
formLoading.value = false
showDialog.value = false
ElMessage.success('添加成功')
paginationChange()
} catch (error) {
formLoading.value = false
}
}
})
}
onMounted(async () => {
await getMenuGrounp()
await getNavCodes()
await appMenuPageAjax()
})
</script>
<style scoped lang="scss">
.table {
height: calc(100vh - 356px);
}
</style>

12
src/views/demo/demo11.vue Normal file
View File

@@ -0,0 +1,12 @@
<template>
<div class="demo">
<h2>demo11</h2>
</div>
</template>
<script setup>
let aa = ref('')
</script>
<style lang="scss" scoped></style>

12
src/views/demo/demo12.vue Normal file
View File

@@ -0,0 +1,12 @@
<template>
<div class="demo">
<h2>demo12</h2>
<router-view></router-view>
</div>
</template>
<script setup>
let aa = ref("");
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,11 @@
<template>
<div class="demo">
<h2>demo121</h2>
</div>
</template>
<script setup>
let aa = ref("");
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,9 @@
<template>
<div class="demo">
<h2>demo122</h2>
</div>
</template>
<script setup></script>
<style lang="scss" scoped></style>

12
src/views/demo/demo13.vue Normal file
View File

@@ -0,0 +1,12 @@
<template>
<div class="demo">
<h2>demo13</h2>
</div>
</template>
<script setup>
let aa = ref('')
</script>
<style lang="scss" scoped></style>

11
src/views/demo/demo21.vue Normal file
View File

@@ -0,0 +1,11 @@
<template>
<div class="demo">
<h2>demo21</h2>
</div>
</template>
<script setup>
let aa = ref('')
</script>
<style lang="scss" scoped></style>

11
src/views/demo/demo22.vue Normal file
View File

@@ -0,0 +1,11 @@
<template>
<div class="demo">
<h2>demo22</h2>
</div>
</template>
<script setup>
let aa = ref('')
</script>
<style lang="scss" scoped></style>

11
src/views/demo/demo23.vue Normal file
View File

@@ -0,0 +1,11 @@
<template>
<div class="demo">
<h2>demo23</h2>
</div>
</template>
<script setup>
let aa = ref('')
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,200 @@
<template>
<div class="card">
<div class="data_row">
<div class="item">
<div class="icon">
<img class="img" src="@/assets/icon_dev1.png">
</div>
<div class="info">
<div class="title">总设备数</div>
<div class="num">
<count-to :start-val="0" :end-val="deviceData.sumCount" :duration="1000" />
</div>
</div>
</div>
<div class="item">
<div class="icon">
<img class="img" src="@/assets/icon_dev2.png">
</div>
<div class="info">
<div class="title">已激活数</div>
<div class="num">
<count-to :start-val="0" :end-val="deviceData.activityCount" :duration="1000" />
</div>
</div>
</div>
<div class="item">
<div class="icon">
<img class="img" src="@/assets/icon_dev3.png">
</div>
<div class="info">
<div class="title">未激活数</div>
<div class="num">
<count-to :start-val="0" :end-val="deviceData.sellCount" :duration="1000" />
</div>
</div>
</div>
</div>
</div>
<div class="card mt15">
<el-space class="pb15">
<el-input placeholder="请输入商户编号搜索" v-model="tableOptions.merchantCode" style="width: 200px;" />
<el-button type="primary" icon="Search" @click="searchHandle">搜索</el-button>
</el-space>
<div class="table">
<el-table :data="tableOptions.list" size="large" stripe border height="100%" v-loading="tableOptions.loading">
<el-table-column prop="merchantCode" label="商户号"></el-table-column>
<el-table-column prop="snNo" label="设备号"></el-table-column>
<el-table-column prop="actMercName" label="商户名"></el-table-column>
<el-table-column prop="phone" label="联系电话"></el-table-column>
<el-table-column prop="status" label="激活状态">
<template #default="scope">
<el-tag type="success" disable-transitions v-if="scope.row.status == 3">已激活</el-tag>
<el-tag type="warning" disable-transitions v-else>未激活</el-tag>
</template>
</el-table-column>
<el-table-column prop="countSum" label="收单笔数"></el-table-column>
<el-table-column prop="consumeFee" label="收单总额"></el-table-column>
<el-table-column prop="loginName" label="上级名称"></el-table-column>
</el-table>
</div>
<div class="mt15">
<el-pagination layout="prev, pager, next, total, sizes, jumper" background
v-model:current-page="tableOptions.pageNum" v-model:page-size="tableOptions.pageSzie"
:page-size="tableOptions.pageSzie" :page-sizes="[10, 20, 30, 50]" :total="tableOptions.total"
@size-change="paginationChange" @current-change="paginationChange" />
</div>
</div>
</template>
<script setup>
import { onMounted, reactive } from 'vue';
import { getSumDeviceStock, getDeviceStockInfo } from '@/api/device.js'
const deviceData = reactive({
sumCount: 0,
activityCount: 0,
sellCount: 0
})
// 表格参数
const tableOptions = reactive({
loading: true,
merchantCode: '',
list: [],
total: 0,
pageNum: 1,
pageSzie: 10
})
// 搜索
function searchHandle() {
tableOptions.pageNum = 1;
paginationChange()
}
// 分页回调
function paginationChange() {
tableOptions.loading = true
getDeviceStockInfoHandle()
}
// 获取当前用户设备总数
async function getSumDeviceStockHandle() {
try {
const { sumCount, activityCount, sellCount } = await getSumDeviceStock()
deviceData.sumCount = sumCount
deviceData.activityCount = activityCount
deviceData.sellCount = sellCount
} catch (error) { }
}
// 获取设备列表
async function getDeviceStockInfoHandle() {
try {
const { total, list } = await getDeviceStockInfo({
merchantCode: tableOptions.merchantCode,
pageNum: tableOptions.pageNum,
pageSize: tableOptions.pageSzie
})
tableOptions.loading = false
tableOptions.total = total
tableOptions.list = list
} catch (error) {
tableOptions.loading = false
}
}
onMounted(() => {
getSumDeviceStockHandle()
getDeviceStockInfoHandle()
})
</script>
<style scoped lang="scss">
.table {
height: calc(100vh - 456px);
}
.data_row {
display: flex;
padding: 24px 0;
.item {
flex: 1;
display: flex;
padding-left: 50px;
&:not(:last-child) {
position: relative;
&::after {
content: "";
height: 100%;
border-right: 1px solid #ececec;
position: absolute;
top: 0;
right: 0;
}
}
.icon {
$size: 50px;
width: $size;
height: $size;
.img {
width: $size;
height: $size;
}
}
.info {
flex: 1;
padding-left: 20px;
display: flex;
flex-direction: column;
justify-content: center;
.title {
font-size: 14px;
color: #666;
font-weight: 300;
}
.num {
font-size: 24px;
font-weight: bold;
padding-top: 10px;
.i {
font-size: 12px;
position: relative;
bottom: 1px;
right: -4px;
}
}
}
}
}
</style>

10
src/views/error/401.vue Normal file
View File

@@ -0,0 +1,10 @@
<template>
<div class="401">
<h1>401</h1>
</div>
</template>
<script setup>
</script>
<style lang="scss" scoped></style>

44
src/views/error/404.vue Normal file
View File

@@ -0,0 +1,44 @@
<template>
<div class="empty-404">
<img src="../../assets/images/404.png">
<p class="tips">您访问的页面不存在</p>
<el-button @click="goRoute">{{ countDown }}s 返回</el-button>
</div>
</template>
<script setup>
const router = useRouter()
function goRoute() {
router.push('/home');
}
let countDown = ref(5)
let timer = setInterval(() => {
countDown.value--
if (countDown.value == 0) {
goRoute()
}
}, 1000)
onUnmounted(() => {
clearInterval(timer)
})
</script>
<style lang="scss" scoped>
.empty-404 {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.tips {
font-size: 40px;
line-height: 100px;
}
}
</style>

215
src/views/home.vue Normal file
View File

@@ -0,0 +1,215 @@
<template>
<div class="card">
<div class="header_title">
欢迎回来{{ storeUser.userInfo.loginName }}
</div>
<div class="data_row">
<div class="item">
<div class="icon">
<img class="img" src="../assets/home_icon1.png">
</div>
<div class="info">
<div class="title">团队总流水</div>
<div class="num">
<count-to :start-val="0" :decimals="2" :end-val="homeData.sumConsumeFee" :duration="1000" />
<!-- {{ homeData.sumConsumeFee }} -->
<span class="i"></span>
</div>
</div>
</div>
<div class="item">
<div class="icon">
<img class="img" src="../assets/home_icon2.png">
</div>
<div class="info">
<div class="title">总收益</div>
<div class="num">
<count-to :start-val="0" :decimals="2" :end-val="homeData.sumfansShareMoney" :duration="1000" />
<span class="i"></span>
</div>
</div>
</div>
<div class="item">
<div class="icon">
<img class="img" src="../assets/home_icon3.png">
</div>
<div class="info">
<div class="title">今日收益</div>
<div class="num">
<count-to :start-val="0" :decimals="2" :end-val="homeData.yestedayShareMoney" :duration="1000" />
<span class="i"></span>
</div>
</div>
</div>
<div class="item">
<div class="icon">
<img class="img" src="../assets/home_icon4.png">
</div>
<div class="info">
<div class="title">推广费率</div>
<div class="num">
{{ homeData.currentFee }}
<span class="i">%</span>
</div>
</div>
</div>
</div>
</div>
<div class="card mt15">
<chart-card :user-id="storeUser.userInfo.userId" />
</div>
</template>
<script setup>
import { getIndexData } from '@/api/home.js'
import { useUser } from "@/store/user.js";
import chartCard from '@/components/chartCard.vue';
const storeUser = useUser();
let homeData = reactive({
sumConsumeFee: 0,
sumfansShareMoney: 0,
yestedayShareMoney: 0,
currentFee: 0
})
// 获取首页数据
async function getIndexDataHandle() {
try {
const res = await getIndexData()
homeData.sumConsumeFee = res.sumConsumeFee
homeData.sumfansShareMoney = res.sumfansShareMoney
homeData.yestedayShareMoney = res.yestedayShareMoney
homeData.currentFee = res.currentFee
} catch (error) {
console.log(error)
}
}
onMounted(() => {
getIndexDataHandle()
});
</script>
<style lang="scss" scoped>
.header_title {
font-size: 18px;
font-weight: bold;
padding: 15px 0 30px 0;
border-bottom: 1px solid #ececec;
}
.data_row {
display: flex;
padding: 50px 0;
.item {
flex: 1;
display: flex;
padding-left: 50px;
&:not(:last-child) {
position: relative;
&::after {
content: "";
height: 100%;
border-right: 1px solid #ececec;
position: absolute;
top: 0;
right: 0;
}
}
.icon {
$size: 55px;
width: $size;
height: $size;
border-radius: 50%;
background-color: #F2F3F5;
display: flex;
justify-content: center;
align-items: center;
.img {
$size: 30px;
width: $size;
height: $size;
object-fit: contain;
}
}
.info {
flex: 1;
padding-left: 20px;
display: flex;
flex-direction: column;
justify-content: center;
.title {
font-size: 14px;
color: #666;
font-weight: 300;
}
.num {
font-size: 24px;
font-weight: bold;
padding-top: 10px;
.i {
font-size: 12px;
position: relative;
bottom: 1px;
right: -4px;
}
}
}
}
}
.userInfo {
padding: 20px;
width: 100%;
display: flex;
align-items: center;
gap: 0 20px;
border: 1px solid #eee;
background-color: #fff;
.name {
font-size: 20px;
font-weight: 600;
}
}
.charts-case {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: repeat(2, 1fr);
gap: 10px;
grid-template-areas:
"a b"
"c c";
margin-top: 10px;
}
.charts-chunk {
border: 1px solid #eee;
background-color: #fff;
}
.charts-chunk:nth-child(0) {
grid-area: a;
}
.charts-chunk:nth-child(1) {
grid-area: b;
}
.charts-chunk:last-child {
grid-area: c;
}
</style>

View File

@@ -0,0 +1,3 @@
<template>
<router-view></router-view>
</template>

View File

@@ -0,0 +1,219 @@
<template>
<el-drawer class="vab-drawer" size="400px" :model-value="props.modelValue" :before-close="close">
<template #header>
<h4>配置</h4>
</template>
<template #default>
<el-divider>
<span>布局切换</span>
</el-divider>
<el-row :gutter="10" class="vab-row">
<el-col :span="9" class="list-title"> 布局 </el-col>
<el-col :span="15">
<el-select v-model="getConfigure.menuMode" placeholder="请选择布局" @change="changeMenuMode(getConfigure.menuMode)">
<el-option v-for="(item, index) in layoutModeEnum.key" :key="item" :label="layoutModeEnum.value[index]" :value="item" />
</el-select>
</el-col>
</el-row>
<el-divider>
<span>主题配置</span>
</el-divider>
<el-row :gutter="10" class="vab-row">
<el-col :span="9" class="list-title"> 主题色 </el-col>
<el-col :span="15">
<el-color-picker v-model="getConfigure.themeColor" @change="setConfigure('themeColor')" />
</el-col>
</el-row>
<el-divider>
<span>标签</span>
</el-divider>
<el-row :gutter="10" class="vab-row">
<el-col :span="9" class="list-title"> 标签风格 </el-col>
<el-col :span="15">
<el-select v-model="getConfigure.navbarMode" placeholder="请选择布局">
<el-option v-for="(item, index) in navbarModeEnum.key" :key="item" :label="navbarModeEnum.value[index]" :value="item" />
</el-select>
</el-col>
</el-row>
<el-row :gutter="10" class="vab-row">
<el-col :span="9" class="list-title"> 标签图标 </el-col>
<el-col :span="15">
<el-switch v-model="getConfigure.navbarIcon" />
</el-col>
</el-row>
<el-divider>
<span>菜单</span>
</el-divider>
<el-row :gutter="10" class="vab-row">
<el-col :span="9" class="list-title"> 菜单背景 </el-col>
<el-col :span="15">
<el-color-picker v-if="getConfigure.menuMode == layoutModeEnum.key[0]" v-model="getConfigure.menuBGColor" @change="setConfigure('menuBGColor')" />
<el-color-picker v-else-if="getConfigure.menuMode == layoutModeEnum.key[1]" v-model="getConfigure.columnBgColor" @change="setConfigure('columnBgColor')" />
</el-col>
</el-row>
<el-row :gutter="10" class="vab-row">
<el-col :span="9" class="list-title"> 菜单文字颜色 </el-col>
<el-col :span="15">
<el-color-picker v-model="getConfigure.textColor" @change="setConfigure('textColor')" />
</el-col>
</el-row>
<el-row :gutter="10" class="vab-row">
<el-col :span="9" class="list-title"> 菜单活跃文字颜色 </el-col>
<el-col :span="15">
<el-color-picker v-model="getConfigure.activeTextColor" @change="setConfigure('activeTextColor')" />
</el-col>
</el-row>
<el-row :gutter="10" class="vab-row">
<el-col :span="9" class="list-title">菜单的 Logo</el-col>
<el-col :span="15">
<el-switch v-model="getConfigure.showMenuLogo" />
</el-col>
</el-row>
<el-divider>
<span>过度</span>
</el-divider>
<el-row :gutter="10" class="vab-row">
<el-col :span="9" class="list-title">
组件切换过度
<el-tooltip class="box-item" effect="dark" content="开启组件切换过渡时,组件只能有一个根元素。" placement="top">
<SvgIcon name="QuestionFilled" color="#909399" style="margin-left: 5px"></SvgIcon>
</el-tooltip>
</el-col>
<el-col :span="15">
<el-switch v-model="getConfigure.componentTransition" />
</el-col>
</el-row>
<el-row :gutter="10" class="vab-row">
<el-col :span="9" class="list-title">组件过渡</el-col>
<el-col :span="15">
<el-select v-model="getConfigure.componentTransitionMode" placeholder="请选择过度模式">
<el-option v-for="(item, index) in componentTransitionEnum.key" :key="item" :label="componentTransitionEnum.value[index]" :value="item" />
</el-select>
</el-col>
</el-row>
</template>
<template #footer>
<div style="flex: auto">
<el-button type="primary" @click="confirm">保存</el-button>
<el-button @click="recovery">恢复默认</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup>
import { useConfigure } from "@/store/configure.js";
// 控制组件的显示
const props = defineProps({
modelValue: {
type: Boolean,
required: true,
},
});
const emits = defineEmits(["update:modelValue"]);
function close() {
emits("update:modelValue", !props.modelValue);
}
// 获取配置
const storeConfigure = useConfigure();
const { configure } = storeToRefs(storeConfigure);
const getConfigure = computed(() => {
return configure.value;
});
// 获取枚举值
const { layoutModeEnum, navbarModeEnum, setColorEnum, componentTransitionEnum } = ENUMS;
// 切换侧边导航的布局
function changeMenuMode(mode) {
switch (mode) {
case layoutModeEnum.key[0]:
_hook.useCssVar("--el-menu-bg-color", storeConfigure.configure.menuBGColor);
break;
case layoutModeEnum.key[1]:
_hook.useCssVar("--el-menu-bg-color", "#ffffff");
break;
default:
}
}
// 设置配置
function setConfigure(type) {
_hook.useCssVar(setColorEnum[type], getConfigure.value[type]);
switch (type) {
case "themeColor":
_hook.useCssVar("--el-menu-hover-bg-color", storeConfigure.configure.themeColor);
[3, 5, 7, 8, 9].forEach((i) => {
_hook.useCssVar(`--el-color-primary-light-${i}`, _hook.useLightColor(getConfigure.value[type], `0.${i}`));
});
break;
case "menuBGColor":
storeConfigure.change("columnBgColor", storeConfigure.configure.menuBGColor);
_hook.useCssVar("--admin-column-bg-color", storeConfigure.configure.menuBGColor);
break;
case "columnBgColor":
storeConfigure.change("menuBGColor", storeConfigure.configure.columnBgColor);
break;
case "activeTextColor":
_hook.useCssVar("--el-menu-hover-text-color", storeConfigure.configure.activeTextColor);
break;
default:
}
}
// 保存配置
function confirm() {
_hook.useLocalStorage.set("configure", storeConfigure.configure);
ElMessage({
message: "配置保存成功",
type: "success",
});
}
// 恢复默认
function recovery() {
_hook.useLocalStorage.remove("configure");
storeConfigure.$reset();
// 初始化颜色
_hook.useCssVar("--el-color-primary", storeConfigure.configure.themeColor);
switch (storeConfigure.configure.menuMode) {
case layoutModeEnum.key[0]:
_hook.useCssVar("--el-menu-bg-color", storeConfigure.configure.menuBGColor);
break;
case layoutModeEnum.key[1]:
_hook.useCssVar("--el-menu-bg-color", "#ffffff");
break;
default:
}
_hook.useCssVar("--admin-column-bg-color", storeConfigure.configure.menuBGColor);
_hook.useCssVar("--el-menu-text-color", storeConfigure.configure.textColor);
_hook.useCssVar("--el-menu-active-color", storeConfigure.configure.activeTextColor);
[3, 5, 7, 8, 9].forEach((i) => {
_hook.useCssVar(`--el-color-primary-light-${i}`, _hook.useLightColor(storeConfigure.configure.themeColor, `0.${i}`));
});
ElMessage({
message: "配置已恢复默认",
type: "success",
});
}
</script>
<style lang="scss" scoped>
.vab-row {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.list-title {
display: flex;
align-items: center;
font-size: 15px;
line-height: 1;
color: --el-text-color-secondary;
}
</style>

View File

@@ -0,0 +1,121 @@
<template>
<div class="navbar-case">
<Transition name="navbar" mode="out-in">
<navbar-mode-a v-if="configure.navbarMode == navbarModeEnum.key[0]"></navbar-mode-a>
<navbar-mode-b v-else-if="configure.navbarMode == navbarModeEnum.key[1]"></navbar-mode-b>
<navbar-mode-c v-else-if="configure.navbarMode == navbarModeEnum.key[2]"></navbar-mode-c>
</Transition>
<div class="menu">
<el-dropdown @visible-change="changeDropdownVisible">
<div class="flex align-center" style="gap: 0 10px">
<SvgIcon class="menu-icon" :style="menuIconStyle" name="Menu"></SvgIcon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item in menuItem" :key="item.title" @click="setNavList(item.type)">
<SvgIcon :name="item.icon"></SvgIcon>
<span>{{ item.title }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup>
import navbarModeA from "./navbar-modeA.vue";
import navbarModeB from "./navbar-modeB.vue";
import navbarModeC from "./navbar-modeC.vue";
import { useConfigure } from "@/store/configure.js";
import { useRoutes } from "@/store/routes.js";
const storeRoutes = useRoutes();
// 获取枚举值
const { navbarModeEnum } = ENUMS;
// 获取配置
const storeConfigure = useConfigure();
const { configure } = storeToRefs(storeConfigure);
// 下拉菜单
let menuItem = [
{
title: "关闭其他",
icon: "Close",
type: "else",
},
{
title: "关闭左侧",
icon: "Back",
type: "left",
},
{
title: "关闭右侧",
icon: "Right",
type: "right",
},
{
title: "关闭全部",
icon: "Close",
type: "all",
},
];
let menuIconStyle = reactive({
transform: `rotate(0deg)`,
color: "",
});
/**
* @description: 下拉菜单的显示与隐藏
* @param {Bool} bool: true显示 false隐藏
*/
function changeDropdownVisible(bool) {
menuIconStyle.transform = bool ? "rotate(45deg)" : "rotate(0deg)";
menuIconStyle.color = bool ? storeConfigure.configure.themeColor : "";
}
// 设置 nav 的列表
const router = useRouter();
function setNavList(type) {
storeRoutes.setNavList(type).then(() => {
router.push("/");
});
}
</script>
<style lang="scss" scoped>
// 导航栏动画
.navbar-enter-active,
.navbar-leave-active {
transition: opacity 0.3s ease;
}
.navbar-enter-from,
.navbar-leave-to {
opacity: 0;
}
.navbar-case {
display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--el-color-info-light-9);
}
.menu {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
// background-color: var(--el-bg-color);
width: 50px;
height: 50px;
.menu-icon {
font-size: 20px;
color: var(--el-text-color-secondary);
transition: all 0.3s;
}
}
</style>

View File

@@ -0,0 +1,169 @@
<template>
<div class="navBar-list-mode-a">
<div :class="[storeRoutes.activeRoute == item[0] ? 'list-active' : 'list']" @click="goRoute(item[0])"
v-for="item in storeRoutes.navList">
<SvgIcon class="list-icon" v-if="storeConfigure.configure.navbarIcon" :name="item[1]?.meta?.icon"></SvgIcon>
<span class="list-title">{{ item[1]?.meta?.title || item[0] }}</span>
<SvgIcon class="list-close" name="Close" v-if="item[0] !== '/home'" @click.stop="closeNavbarItem(item[0])">
</SvgIcon>
</div>
</div>
</template>
<script setup>
import { useRoutes } from '@/store/routes.js'
import { useConfigure } from '@/store/configure.js'
const storeRoutes = useRoutes()
const storeConfigure = useConfigure()
const router = useRouter()
/**
* @description: 路由跳转
* @param {String} route: 要跳转的路由
*/
function goRoute(route) {
router.push(route)
}
/**
* @description: 关闭导航的某一项
* @param {String} route: 要关闭的当前项
*/
function closeNavbarItem(route) {
storeRoutes.deleteNavItem(route).then(res => {
goRoute(res[0])
})
}
</script>
<style lang="scss" scoped>
.navBar-list-mode-a {
flex-grow: 1;
display: flex;
align-items: flex-end;
padding: 0 30px;
height: 50px;
overflow: hidden;
&:hover {
overflow: overlay;
overflow-y: hidden;
&::-webkit-scrollbar {
height: 6px;
background-color: #f1f1f1;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: #ccc;
border-radius: 4px;
}
}
}
.list-icon {
font-size: 16px;
bottom: -0.2em;
color: var(--el-text-color-primary);
}
.list-title {
padding: 0 5px;
font-size: 14px;
color: var(--el-text-color-primary);
}
.list-close {
font-size: 14px;
transform: scale(0, 0);
transition: all 0.3s;
bottom: -0.18em;
color: var(--el-text-color-primary);
}
.list {
position: relative;
cursor: pointer;
flex-shrink: 0;
text-align: center;
overflow: visible;
line-height: 16px;
font-size: 16px;
padding: 0 10px;
display: inline-block;
height: 35px;
line-height: 35px;
transition: all .3s cubic-bezier(.645, .045, .355, 1) !important;
&:hover {
padding: 0 20px;
&::after {
z-index: -1;
content: ' ';
position: absolute;
left: -10px;
right: -10px;
height: 100%;
background-color: var(--el-color-info-light-7);
-webkit-mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANoAAAAkBAMAAAAdqzmBAAAAMFBMVEVHcEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlTPQ5AAAAD3RSTlMAr3DvEM8wgCBA379gj5//tJBPAAAAnUlEQVRIx2NgAAM27fj/tAO/xBsYkIHyf9qCT8iWMf6nNQhAsk2f5rYheY7Dnua2/U+A28ZEe8v+F9Ax2v7/F4DbxkUH2wzgtvHTwbYPo7aN2jZq26hto7aN2jZq25Cy7Qvctnw62PYNbls9HWz7S8/G6//PsI6H4396gAUQy1je08W2jxDbpv6nD4gB2uWp+J9eYPsEhv/0BPS1DQBvoBLVZ3BppgAAAABJRU5ErkJggg==);
mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANoAAAAkBAMAAAAdqzmBAAAAMFBMVEVHcEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlTPQ5AAAAD3RSTlMAr3DvEM8wgCBA379gj5//tJBPAAAAnUlEQVRIx2NgAAM27fj/tAO/xBsYkIHyf9qCT8iWMf6nNQhAsk2f5rYheY7Dnua2/U+A28ZEe8v+F9Ax2v7/F4DbxkUH2wzgtvHTwbYPo7aN2jZq26hto7aN2jZq25Cy7Qvctnw62PYNbls9HWz7S8/G6//PsI6H4396gAUQy1je08W2jxDbpv6nD4gB2uWp+J9eYPsEhv/0BPS1DQBvoBLVZ3BppgAAAABJRU5ErkJggg==);
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
.list-close {
transform: scale(1, 1);
&:hover {
background-color: var(--el-text-color-primary);
color: #fff;
border-radius: 50%;
}
}
}
}
.list-active {
position: relative;
cursor: pointer;
flex-shrink: 0;
overflow: visible;
color: var(--el-color-primary);
padding: 0 20px;
display: inline-block;
height: 35px;
line-height: 35px;
&::before {
z-index: -1;
content: ' ';
position: absolute;
left: -10px;
right: -10px;
height: 100%;
background-color: var(--el-color-primary-light-9);
-webkit-mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANoAAAAkBAMAAAAdqzmBAAAAMFBMVEVHcEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlTPQ5AAAAD3RSTlMAr3DvEM8wgCBA379gj5//tJBPAAAAnUlEQVRIx2NgAAM27fj/tAO/xBsYkIHyf9qCT8iWMf6nNQhAsk2f5rYheY7Dnua2/U+A28ZEe8v+F9Ax2v7/F4DbxkUH2wzgtvHTwbYPo7aN2jZq26hto7aN2jZq25Cy7Qvctnw62PYNbls9HWz7S8/G6//PsI6H4396gAUQy1je08W2jxDbpv6nD4gB2uWp+J9eYPsEhv/0BPS1DQBvoBLVZ3BppgAAAABJRU5ErkJggg==);
mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANoAAAAkBAMAAAAdqzmBAAAAMFBMVEVHcEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlTPQ5AAAAD3RSTlMAr3DvEM8wgCBA379gj5//tJBPAAAAnUlEQVRIx2NgAAM27fj/tAO/xBsYkIHyf9qCT8iWMf6nNQhAsk2f5rYheY7Dnua2/U+A28ZEe8v+F9Ax2v7/F4DbxkUH2wzgtvHTwbYPo7aN2jZq26hto7aN2jZq25Cy7Qvctnw62PYNbls9HWz7S8/G6//PsI6H4396gAUQy1je08W2jxDbpv6nD4gB2uWp+J9eYPsEhv/0BPS1DQBvoBLVZ3BppgAAAABJRU5ErkJggg==);
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
.list-icon,
.list-title,
.list-close {
color: inherit;
transform: scale(1, 1);
}
.list-close {
&:hover {
background-color: var(--el-color-primary);
color: #fff;
border-radius: 50%;
}
}
}
</style>

View File

@@ -0,0 +1,138 @@
<template>
<div class="navBar-list-mode-b">
<div class="list" :class="[storeRoutes.activeRoute == item[0] ? 'list-active' : '']" @click="goRoute(item[0])"
v-for="item in storeRoutes.navList">
<SvgIcon class="list-icon" v-if="storeConfigure.configure.navbarIcon" :name="item[1]?.meta?.icon"></SvgIcon>
<span class="list-title">{{ item[1]?.meta?.title || item[0] }}</span>
<SvgIcon class="list-close" name="Close" v-if="item[0] !== '/home'" @click.stop="closeNavbarItem(item[0])">
</SvgIcon>
</div>
</div>
</template>
<script setup>
import { useRoutes } from '@/store/routes.js'
import { useConfigure } from '@/store/configure.js'
const storeRoutes = useRoutes()
const storeConfigure = useConfigure()
const router = useRouter()
/**
* @description: 路由跳转
* @param {String} route: 要跳转的路由
*/
function goRoute(route) {
router.push(route)
}
/**
* @description: 关闭导航的某一项
* @param {String} route: 要关闭的当前项
*/
function closeNavbarItem(route) {
storeRoutes.deleteNavItem(route).then(res => {
goRoute(res[0])
})
}
</script>
<style lang="scss" scoped>
.navBar-list-mode-b {
flex-grow: 1;
display: flex;
align-items: center;
gap: 0 5px;
padding: 0 15px;
height: 60px;
overflow: hidden;
&:hover {
overflow: overlay;
overflow-y: hidden;
&::-webkit-scrollbar {
height: 6px;
background-color: #f1f1f1;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: #ccc;
border-radius: 4px;
}
}
}
.list-icon {
font-size: 16px;
line-height: 1;
color: var(--el-text-color-primary);
bottom: 0.05em;
}
.list-title {
padding: 0 5px;
font-size: 14px;
line-height: 1;
color: var(--el-text-color-primary);
}
.list-close {
font-size: 0px;
transform: scale(0, 0);
transition: all 0.3s;
color: var(--el-text-color-primary);
}
.list {
cursor: pointer;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
height: 30px;
padding: 0 10px;
border: 1px solid var(--el-color-info-light-7);
background-color: #fff;
border-radius: 4px;
&.list-active {
color: var(--el-color-primary);
border: 1px solid var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
&:hover {
background-color: var(--el-color-primary);
color: #fff;
}
}
&:hover {
background-color: var(--el-color-info-light-7);
.list-icon,
.list-title,
.list-close {
color: inherit;
}
.list-close {
font-size: 14px;
transform: scale(1, 1);
}
}
.list-close {
&:hover {
background-color: var(--el-text-color-primary);
color: #fff;
border-radius: 50%;
}
}
}
</style>

View File

@@ -0,0 +1,176 @@
<template>
<div class="navBar-list-mode-c">
<div :class="[storeRoutes.activeRoute == item[0] ? 'list-active' : 'list']" @click="goRoute(item[0])" v-for="item in storeRoutes.navList">
<SvgIcon class="list-icon" v-if="storeConfigure.configure.navbarIcon" :name="item[1]?.meta?.icon"></SvgIcon>
<span class="list-title">{{ item[1]?.meta?.title || item[0] }}</span>
<SvgIcon class="list-close" name="Close" v-if="item[0] !== '/home'" @click.stop="closeNavbarItem(item[0])"> </SvgIcon>
</div>
</div>
</template>
<script setup>
import { useRoutes } from "@/store/routes.js";
import { useConfigure } from "@/store/configure.js";
const storeRoutes = useRoutes();
const storeConfigure = useConfigure();
const router = useRouter();
/**
* @description: 路由跳转
* @param {String} route: 要跳转的路由
*/
function goRoute(route) {
router.push(route);
}
/**
* @description: 关闭导航的某一项
* @param {String} route: 要关闭的当前项
*/
function closeNavbarItem(route) {
storeRoutes.deleteNavItem(route).then((res) => {
goRoute(res[0]);
});
}
</script>
<style lang="scss" scoped>
.navBar-list-mode-c {
flex-grow: 1;
display: flex;
align-items: center;
gap: 0 5px;
padding: 0 30px;
height: 50px;
overflow: hidden;
&:hover {
overflow: overlay;
overflow-y: hidden;
&::-webkit-scrollbar {
height: 6px;
background-color: #f1f1f1;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: #ccc;
border-radius: 4px;
}
}
}
.list-icon {
font-size: 16px;
line-height: 1;
color: var(--el-text-color-primary);
bottom: 0.05em;
}
.list-title {
padding: 0 5px;
font-size: 14px;
line-height: 1;
color: var(--el-text-color-primary);
}
.list-close {
font-size: 0px;
transform: scale(0, 0);
transition: all 0.3s;
color: var(--el-text-color-primary);
}
.list {
position: relative;
cursor: pointer;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
height: 35px;
padding: 0 10px;
&::after {
content: " ";
position: absolute;
bottom: 0;
left: 0;
height: 2px;
width: 0;
background-color: var(--el-color-primary);
transition: width 0.3s;
}
&:hover {
background-color: var(--el-color-primary-light-9);
.list-icon,
.list-title,
.list-close {
color: var(--el-color-primary);
}
.list-close {
font-size: 14px;
transform: scale(1, 1);
}
&::after {
width: 100%;
}
}
.list-close {
&:hover {
background-color: var(--el-color-primary);
color: #fff;
border-radius: 50%;
}
}
}
.list-active {
cursor: pointer;
position: relative;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--el-color-primary);
height: 35px;
padding: 0 10px;
background-color: var(--el-color-primary-light-9);
&::after {
content: " ";
position: absolute;
bottom: 0;
left: 0;
height: 2px;
width: 100%;
background-color: var(--el-color-primary);
}
.list-icon,
.list-title,
.list-close {
color: inherit;
transform: scale(1, 1);
}
.list-close {
font-size: 14px;
&:hover {
background-color: var(--el-color-primary);
color: #fff;
border-radius: 50%;
}
}
}
</style>

View File

@@ -0,0 +1,139 @@
<template>
<div class="pageHeader">
<div class="routes-case">
<SvgIcon class="sidebar-icon" :name="storeConfigure.configure.collapse ? 'Expand' : 'Fold'" size="20" color="#555"
@click="changeSidebar"></SvgIcon>
<el-breadcrumb separator="/">
<!-- <transition-group name="breadcrumb"> -->
<el-breadcrumb-item v-for="item in storeRoutes.breadcrumb" :key="item.path">
<span style="color: #333;">{{ item?.meta?.title || item.path
}}</span></el-breadcrumb-item>
<!-- </transition-group> -->
</el-breadcrumb>
</div>
<div class="info-case">
<!-- <SvgIcon class="info-icon" name="Operation" @click="operationFunction('operation')"></SvgIcon> -->
<SvgIcon class="info-icon" name="Refresh" @click="operationFunction('refresh')"></SvgIcon>
<el-dropdown style="cursor: pointer" @visible-change="changeDropdownVisible">
<div class="flex align-center" style="gap: 0 10px">
<!-- <img class="info-avatar" :src="storeUser.userInfo.avatar" alt="" srcset="" /> -->
<p class="info-name">{{ storeUser.userInfo.loginName }}</p>
<SvgIcon color="#333" name="arrow-down"></SvgIcon>
</div>
<template #dropdown>
<el-dropdown-menu>
<!-- <el-dropdown-item>个人中心</el-dropdown-item> -->
<el-dropdown-item @click="logOut">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup>
import { useConfigure } from "@/store/configure.js";
import { useUser } from "@/store/user.js";
import { useRoutes } from "@/store/routes.js";
const storeUser = useUser();
const storeRoutes = useRoutes();
// 获取配置
const storeConfigure = useConfigure();
// 侧边栏导航的收起与展开
function changeSidebar() {
storeConfigure.change("collapse", !storeConfigure.configure.collapse);
}
// 下拉菜单
let showDropdown = ref(false);
let rotateUserIcon = ref("0deg");
function changeDropdownVisible() {
showDropdown.value = !showDropdown.value;
rotateUserIcon.value = showDropdown.value ? "180deg" : "0deg";
}
// info 中的按钮操作
const emits = defineEmits(["operation"]);
function operationFunction(type) {
emits("operation", type);
}
// 退出登录
function logOut() {
// 清除缓存 / token 等
_hook.useLocalStorage.clear();
// 使用 reload 时,不需要调用 resetRoute() 重置路由
// 且刷新页面时 pinia 数据会重置
window.location.reload();
}
</script>
<style lang="scss" scoped>
// 面包屑的动画
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all 0.5s ease;
}
.breadcrumb-enter-from,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-leave-active {
position: absolute;
z-index: -1;
transition: all 0s ease;
}
.pageHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
height: 60px;
// border-bottom: 1px solid #efefef;
}
.routes-case {
display: flex;
align-items: center;
justify-content: center;
.sidebar-icon {
margin-right: 10px;
}
}
.info-case {
display: flex;
align-items: center;
gap: 0 16px;
.info-icon {
cursor: pointer;
font-size: 16px;
color: #333;
transition: all 0.3s;
&:hover {
color: var(--el-color-primary);
transform: scale(1.2, 1.2);
}
}
.info-avatar {
border-radius: 2px;
width: 35px;
height: 35px;
}
.info-name {
font-size: 14px;
color: #333;
}
}
</style>

View File

@@ -0,0 +1,124 @@
<template>
<template v-if="props.item.children">
<el-sub-menu
:class="{ 'menu-sub-modeA': configure.menuMode == layoutModeEnum.key[0], 'menu-sub-modeB': configure.menuMode == layoutModeEnum.key[1] }"
v-if="!props.item.meta?.isHide" :index="props.item?.path">
<template #title>
<SvgIcon class="route-icon" :name="props.item?.meta?.icon"></SvgIcon>
<span class="route-title">{{ props.item?.meta?.title || props.item?.path }}</span>
</template>
<sidebar-modeA-sub v-for="route in props.item?.children" :item="route" :key="route.path"></sidebar-modeA-sub>
</el-sub-menu>
<template v-else>
<sidebar-modeA-sub v-for="route in props.item?.children" :item="route" :key="route.path"></sidebar-modeA-sub>
</template>
</template>
<template v-else>
<el-menu-item
:class="{ 'active-menu': activeMenuCount(), 'is-active': activeMenuCount(), 'menu-modeA': configure.menuMode == layoutModeEnum.key[0], 'menu-modeB': configure.menuMode == layoutModeEnum.key[1] }"
v-if="!props.item?.meta?.isHide" :index="props.item?.path">
<SvgIcon class="route-icon" :name="props.item?.meta?.icon"></SvgIcon>
<span class="route-title">{{ props.item?.meta?.title || props.item?.path }}</span>
</el-menu-item>
</template>
</template>
<script setup>
import { useRoutes } from "@/store/routes.js";
import { useConfigure } from "@/store/configure.js";
import { useRoute } from 'vue-router'
const storeRoutes = useRoutes();
const route = useRoute();
// 获取配置
const storeConfigure = useConfigure();
const { configure } = storeToRefs(storeConfigure);
const props = defineProps({
item: {
type: Object,
required: true,
},
});
// 获取枚举值
const { layoutModeEnum } = ENUMS;
function activeMenuCount() {
if (route.meta.activeMenu) {
return props.item?.path == route.meta.activeMenu
} else {
return storeRoutes.activeRoute == props.item?.path
}
}
</script>
<style lang="scss">
.menu-sub-modeA {
.el-sub-menu__title {
&:hover {
background-color: rgba(0, 0, 0, 0);
}
}
}
.menu-sub-modeB {
.el-sub-menu__title {
margin: 0 5px;
color: var(--el-text-color-primary);
&:hover {
background-color: rgba(0, 0, 0, 0);
}
}
.el-icon {
color: var(--el-text-color-primary);
}
}
</style>
<style lang="scss" scoped>
.menu-modeA {
&:hover {
// color: var(--el-menu-active-color);
// background-color: var(--el-color-primary-light-3);
background-color: #efefef;
}
}
.menu-modeA.active-menu {
background-color: var(--el-color-primary);
}
.menu-modeB {
margin: 5px;
border-radius: 5px;
.route-title,
.route-icon {
color: var(--el-text-color-primary);
}
&:hover {
background-color: var(--el-color-primary-light-9);
.route-title,
.route-icon {
color: var(--el-color-primary-light-3);
}
}
}
.menu-modeB.active-menu {
background-color: var(--el-color-primary-light-9);
.route-title,
.route-icon {
color: var(--el-color-primary-light-3);
}
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div class="sidebar-mode-a">
<div class="logo-case" v-if="configure.showMenuLogo">
<img class="logo" src="/logo.png" alt="logo" srcset="" />
<transition name="el-fade-in">
<span v-show="!storeConfigure.configure.collapse" style="margin-left: 10px;">{{ storeConfigure.projectName
}}</span>
</transition>
</div>
<el-menu @select="goRoute" :collapse="configure.collapse" :default-active="defaultActive" style="border-right: none"
class="el-menu-vertical-demo">
<sidebar-modea-sub v-for="route in props.list" :key="route.path" :item="route" />
</el-menu>
</div>
</template>
<script setup>
import { useConfigure } from "@/store/configure.js";
import sidebarModeaSub from "./sidebar-modeA-sub.vue";
const props = defineProps({
list: {
type: Array,
default: () => {
return [];
},
required: true,
},
width: {
type: String,
required: true,
},
});
let width = ref(props.width);
// 获取配置
const storeConfigure = useConfigure();
const { configure, defaultActive } = storeToRefs(storeConfigure);
/**
* @description: 路由跳转
* @param {String} index: 路由
*/
const router = useRouter();
function goRoute(index) {
router.push(index);
}
</script>
<style lang="scss">
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: v-bind(width);
}
.sidebar-mode-a {
overflow-y: scroll;
&::-webkit-scrollbar {
display: none;
}
}
.logo-case {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 60px;
overflow: hidden;
.logo {
height: 30px;
border-radius: 6px;
}
}
</style>

View File

@@ -0,0 +1,150 @@
<template>
<div class="sidebar-mode-b">
<el-scrollbar class="scrollbar">
<div class="route-list" :class="{ 'active-route-list': storeRoutes.breadcrumbKeys.includes(item.path) }" v-for="item in data.routesList" @click="goRoute(item.path)">
<SvgIcon class="route-icon" size="16" :name="item?.meta?.icon"></SvgIcon>
<span class="route-title">{{ item?.meta?.title || item.path }}</span>
</div>
</el-scrollbar>
<div :class="['sidebar-mode-b-menu', configure.collapse ? 'sidebar-switch' : 'sidebar-open']">
<sidebar-mode-a menu-trigger="hover" :list="data.activeRouteChildren" width="190px"></sidebar-mode-a>
</div>
</div>
</template>
<script setup>
import sidebarModeA from "./sidebar-modeA.vue";
import { useRoutes } from "@/store/routes.js";
import { useConfigure } from "@/store/configure.js";
// 获取配置
const storeConfigure = useConfigure();
const { configure } = storeToRefs(storeConfigure);
const route = useRoute();
// 获取路由 store
const storeRoutes = useRoutes();
const data = reactive({
routesList: [],
activeRouteChildren: [],
});
/**
* @description: 过滤侧栏可显示的一级路由
* @param {Array} routes: 路由列表
* @return {Array} 过滤好的路由
*/
function filterRoute(routes) {
let arr = [];
routes.forEach((i) => {
if (i?.meta?.isHide == false || i?.meta?.isHide == undefined) {
arr.push(i);
} else {
if (i?.children?.length > 0) {
arr = filterRoute(i.children);
}
}
});
return arr;
}
/**
* @description: 路由跳转
* @param {String} index: 路由
*/
const router = useRouter();
function goRoute(index) {
router.push(index);
}
onMounted(() => {
data.routesList = filterRoute(storeRoutes.routesList);
data.activeRouteChildren = storeRoutes.allRoutes[route.matched[1].path].children;
if (data.activeRouteChildren.length == 0) {
storeConfigure.change("collapse", true);
}
});
watch(
() => storeRoutes.breadcrumbKeys,
() => {
data.activeRouteChildren = storeRoutes.allRoutes[route.matched[1].path]?.children;
if (data?.activeRouteChildren?.length == 0) {
storeConfigure.change("collapse", true);
} else {
storeConfigure.change("collapse", false);
}
},
{ immediate: true }
);
</script>
<style lang="scss" scoped>
.sidebar-mode-b {
display: flex;
height: 100%;
}
.scrollbar {
border-right: 1px solid var(--el-border-color-lighter);
}
.route-list {
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
border-radius: 5px;
margin: 5px;
width: 60px;
height: 60px;
&:hover {
background-color: var(--el-color-primary-light-3);
.route-icon,
.route-title {
color: var(--el-menu-active-color);
}
}
.route-icon,
.route-title {
text-align: center;
color: var(--el-menu-text-color);
font-size: 12px;
}
}
.active-route-list {
background-color: var(--el-color-primary-light-3);
.route-icon,
.route-title {
color: var(--el-menu-active-color);
}
}
.sidebar-switch {
width: 0px;
overflow: hidden;
transition: width 0.3s;
}
.sidebar-open {
width: 190px;
overflow: hidden;
transition: width 0.3s;
}
.sidebar-mode-b-menu {
overflow-y: scroll;
background-color: var(--el-color-white);
height: 100%;
&::-webkit-scrollbar {
display: none;
}
}
</style>

202
src/views/layout/layout.vue Normal file
View File

@@ -0,0 +1,202 @@
<template>
<div class="layout-container">
<div class="layout-case">
<div class="sidebar">
<!-- 侧边栏的布局模式 -->
<sidebar-mode-a v-if="getConfigure.menuMode == layoutModeEnum.key[0]" :list="storeRoutes.routesList"
width="260px"></sidebar-mode-a>
<sidebar-mode-b v-else-if="getConfigure.menuMode == layoutModeEnum.key[1]"></sidebar-mode-b>
</div>
<div class="layout-case" style="flex-direction: column">
<header class="header">
<page-header @operation="operation"></page-header>
<navbar></navbar>
</header>
<main class="main">
<!-- <router-view v-slot="{ Component }" v-if="isRefreshRoute">
<transition :name="getConfigure.componentTransitionMode" mode="out-in"
v-if="getConfigure.componentTransition">
<keep-alive :include="storeRoutes.cachedRoute">
<component :is="Component" />
</keep-alive>
</transition>
<keep-alive v-else :include="storeRoutes.cachedRoute">
<component :is="Component" />
</keep-alive>
</router-view> -->
<router-view v-slot="{ Component }">
<!-- <transition :name="getConfigure.componentTransitionMode" mode="out-in"> -->
<!-- <keep-alive :include="storeRoutes.cachedRoute"> -->
<component :is="Component" />
<!-- </keep-alive> -->
<!-- </transition> -->
<!-- <keep-alive v-else :include="storeRoutes.cachedRoute"> -->
<!-- <component v-else :is="Component" /> -->
<!-- </keep-alive> -->
</router-view>
</main>
<div class="version">©银收客 v{{ packageData.version }}</div>
</div>
</div>
<c-configure v-model="bools.showConfigure"></c-configure>
</div>
</template>
<script setup>
import sidebarModeA from "./components/sidebar-modeA.vue";
import sidebarModeB from "./components/sidebar-modeB.vue";
import pageHeader from "./components/pageHeader.vue";
import navbar from "./components/navBar.vue";
import cConfigure from "./components/configure.vue";
import { useConfigure } from "@/store/configure.js";
import { useRoutes } from "@/store/routes.js";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
const storeRoutes = useRoutes();
import packageData from '../../../package.json'
// 获取枚举值
const { layoutModeEnum } = ENUMS;
// 获取配置
const storeConfigure = useConfigure();
const { configure } = storeToRefs(storeConfigure);
const getConfigure = computed(() => {
return configure.value;
});
// 控制操作的布尔值
const bools = reactive({
showConfigure: false,
});
// 页头的操作
function operation(type) {
switch (type) {
case "operation":
bools.showConfigure = true;
break;
case "refresh":
reload();
break;
}
}
// 局部组件刷新
const isRefreshRoute = ref(true);
function reload() {
NProgress.start();
isRefreshRoute.value = false;
nextTick(() => {
isRefreshRoute.value = true;
NProgress.done();
});
}
</script>
<style>
.vab-drawer .el-drawer__header {
margin-bottom: 0;
}
.el-icon .el-sub-menu__icon-arrow {
color: var(--el-text-color-primary);
}
</style>
<style lang="scss" scoped>
.layout-container {
--versionH: 50px;
}
.version {
height: var(--versionH);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #999;
}
// 页面切换的动画 模式 A
.mainA-enter-active,
.mainA-leave-active {
transition: all 0.5s ease;
}
.mainA-enter-from,
.mainA-leave-active {
opacity: 0;
transform: translateY(100px);
}
.mainA-leave-active {
position: absolute;
z-index: -1;
transition: all 0s ease;
}
// 页面切换的动画 模式 B
.mainB-enter-active,
.mainB-leave-active {
transition: all 0.5s ease;
}
.mainB-enter-from,
.mainB-leave-active {
opacity: 0;
transform: translateX(100px);
}
.mainB-leave-active {
position: absolute;
z-index: -1;
transition: all 0s ease;
}
.layout-container {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
display: flex;
}
.layout-case {
display: flex;
flex-direction: row;
flex: 1;
flex-basis: auto;
box-sizing: border-box;
min-width: 0;
}
.sidebar {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--admin-column-bg-color);
}
.header {
position: relative;
box-sizing: border-box;
flex-shrink: 0;
}
.main {
height: calc(100vh - 60px * 2 - var(--versionH));
display: block;
flex: 1;
flex-basis: auto;
box-sizing: border-box;
overflow-y: auto;
overflow-x: hidden;
padding: 0 15px 15px;
background: var(--el-color-info-light-9);
@extend .scrollbar-y;
}
</style>

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