Commit 74a2cab7 authored by guoxianhui's avatar guoxianhui

初始化

parent 687777e3
# 使用【演示环境】接口地址
#VITE_API_URL=
# 使用【本地环境]接口地址#http://localhost:8658 http://spms.sit.gdyibo.com.cn/sit-api
VITE_API_URL=http://localhost:8658
# 使用【演示环境】接口地址
#VITE_API_URL=
# 使用【本地环境]接口地址
VITE_API_URL=/sit-api
# 使用【演示环境】接口地址
#VITE_API_URL=
# 使用【本地环境]接口地址
VITE_API_URL=http://spms.sit.gdyibo.com.cn/sit-api
logs
*.log
*.sh
node_modules
.DS_Store
lib
*.md
*.scss
*.woff
*.ttf
.vscode
.idea
dist
mock
public
bin
build
config
index.html
components.d.ts
element.d.ts
src/assets
module.exports = {
root: true,
env: {
node: true
},
extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'],
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2022,
ecmaFeatures: {
jsx: true
}
},
rules: {
curly: 'error', // 控制语句需要大括号
'vue/multi-word-component-names': 'off'
}
}
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# 使用pnpm,则需要
shamefully-hoist=true
// 参考:https://prettier.io/docs/en/options.html
module.exports = {
// 超过指定值换行
printWidth: 150,
// 制表符宽度
tabWidth: 2,
// 使用制表符缩进
useTabs: true,
// 句尾不使用分号
semi: false,
// 使用单引号
singleQuote: true,
// 在 JSX 文件中使用单引号代替双引号
jsxSingleQuote: true,
// 在对象或数组最后一个元素后面是否加逗号,可选值"[none:不追加 | es5:追加]
trailingComma: 'none',
// 在对象,数组括号与代码之间加空格 "{ foo: bar }"
bracketSpacing: true,
// 将多行 JSX 元素的 > 放置于最后一行的末尾,而非换行
jsxBracketSameLine: true,
//(x) => {} 箭头函数参数只有一个时是否要有小括号,可选值"[always:(x) => x | avoid:x => x]
arrowParens: 'avoid',
// 使用默认的折行标准,可选值"[always|never|preserve]"
proseWrap: 'preserve',
// 指定HTML文件的全局空格敏感度,可选值"[css|strict|ignore]"
htmlWhitespaceSensitivity: 'css',
// 结尾时换行符,默认值lf,可选值"[auto|lf|crlf|cr]"
endOfLine: 'auto'
}
MIT License
Copyright (c) 2022 MAKU
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# fast-order-pt-vue
订购平台前端
\ No newline at end of file
## 介绍
- 基于Vue3、TypeScript、Element Plus、Vue Router、Pinia、Axios、i18n、Vite等开发的后台管理
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="Expires" content="0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Cache" content="no-cache">
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title></title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
{
"name": "oms-admin",
"version": "2.5.0",
"scripts": {
"dev": "vite --force",
"build": "vue-tsc --noEmit && vite build",
"build:sit": "vite build --mode sit",
"build:prod": "vite build --mode prod",
"preview": "vite preview",
"lint": "eslint --fix --ext .vue,.jsx,.ts,.tsx ."
},
"dependencies": {
"@element-plus/icons-vue": "2.1.0",
"@kangc/v-md-editor": "2.3.17",
"@vueuse/core": "9.1.1",
"@wangeditor/editor": "5.1.1",
"@wangeditor/editor-for-vue": "5.1.12",
"axios": "0.27.2",
"cropperjs": "1.5.12",
"echarts": "5.4.3",
"element-plus": "2.8.0",
"maku-form-design": "1.1.0",
"mitt": "3.0.0",
"nprogress": "0.2.0",
"pinia": "2.0.30",
"print-js": "1.6.0",
"qrcode.vue": "3.3.3",
"qs": "6.10.3",
"sortablejs": "^1.15.0",
"vform3-builds": "^3.0.10",
"vue": "3.3.4",
"vue-i18n": "9.1.9",
"vue-router": "4.2.5",
"vxe-table": "^4.3.10",
"xe-utils": "^3.5.7"
},
"devDependencies": {
"@babel/types": "7.17.0",
"@types/node": "17.0.41",
"@types/nprogress": "0.2.0",
"@types/qs": "6.9.7",
"@types/sortablejs": "^1.10.7",
"@vitejs/plugin-vue": "4.2.3",
"@vue/compiler-sfc": "3.3.4",
"@vue/eslint-config-prettier": "7.0.0",
"@vue/eslint-config-typescript": "10.0.0",
"@vue/tsconfig": "0.1.3",
"eslint": "8.13.0",
"eslint-plugin-vue": "8.6.0",
"prettier": "2.8.4",
"sass": "1.58.0",
"typescript": "4.7.4",
"vite": "4.4.8",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-setup-extend": "0.4.0",
"vue-tsc": "1.8.16"
}
}
<template>
<el-config-provider :locale="locale" :size="size">
<router-view />
</el-config-provider>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted } from 'vue'
import { RouterView } from 'vue-router'
import { useTitle } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
import { messages } from '@/i18n'
import { handleThemeStyle } from '@/utils/theme'
import { useAppStore } from '@/store/modules/app'
const appStore = useAppStore()
const { t } = useI18n()
const locale = computed(() => messages[appStore.language].el)
const size = computed(() => appStore.componentSize)
// 设置标题
useTitle(t('app.title'))
onMounted(() => {
nextTick(() => {
// 初始化主题样式
handleThemeStyle(appStore.theme)
})
})
</script>
import service from '@/utils/request'
export const useCaptchaEnabledApi = () => {
return service.get('/sys/auth/captcha/enabled')
}
export const useCaptchaApi = () => {
return service.get('/sys/auth/captcha')
}
export const useSendCodeApi = (mobile: string) => {
return service.post('/sys/auth/send/code?mobile=' + mobile)
}
export const useAccountLoginApi = (data: any) => {
return service.post('/sys/auth/login', data, {headers: {'Ebo-Tenant-Code': data.eboTenantCode}})
}
export const useMobileLoginApi = (data: any) => {
return service.post('/sys/auth/mobile', data)
}
export const useLogoutApi = () => {
return service.post('/sys/auth/logout')
}
import service from '@/utils/request'
export const useSmsPlatformApi = (id: Number) => {
return service.get('/message/sms/platform/' + id)
}
export const useSmsPlatformSubmitApi = (dataForm: any) => {
if (dataForm.id) {
return service.put('/message/sms/platform', dataForm)
} else {
return service.post('/message/sms/platform', dataForm)
}
}
export const useSmsSendApi = (dataForm: any) => {
return service.post('/message/sms/platform/send', dataForm)
}
import service from '@/utils/request'
export const useCacheInfoApi = () => {
return service.get('/monitor/cache/info')
}
import service from '@/utils/request'
export const useServerInfoApi = () => {
return service.get('/monitor/server/info')
}
export const useCpuInfoApi = () => {
return service.get('/monitor/server/cpu')
}
export const useMemInfoApi = () => {
return service.get('/monitor/server/mem')
}
export const useJvmInfoApi = () => {
return service.get('/monitor/server/jvm')
}
export const useSysInfoApi = () => {
return service.get('/monitor/server/sys')
}
export const useDiskInfoApi = () => {
return service.get('/monitor/server/disk')
}
import service from '@/utils/request'
export const useMonitorUserLogoutApi = (accessToken: string) => {
return service.delete('/monitor/user/' + accessToken)
}
import service from '@/utils/request'
export const useScheduleApi = (id: Number) => {
return service.get('/schedule/' + id)
}
export const useScheduleSubmitApi = (dataForm: any) => {
if (dataForm.id) {
return service.put('/schedule', dataForm)
} else {
return service.post('/schedule', dataForm)
}
}
export const useScheduleRunApi = (dataForm: any) => {
return service.put('/schedule/run', dataForm)
}
export const useScheduleStatusApi = (dataForm: any) => {
return service.put('/schedule/change-status', dataForm)
}
import service from '@/utils/request'
export const useAttachmentSubmitApi = (dataForm: any) => {
return service.post('/sys/attachment', dataForm)
}
import service from '@/utils/request'
export const useDictTypeAllApi = () => {
return service.get('/sys/dict/type/all')
}
export const useDictTypeApi = (id: Number) => {
return service.get('/sys/dict/type/' + id)
}
export const useDictTypeSubmitApi = (dataForm: any) => {
if (dataForm.id) {
return service.put('/sys/dict/type', dataForm)
} else {
return service.post('/sys/dict/type', dataForm)
}
}
export const useDictDataApi = (id: Number) => {
return service.get('/sys/dict/data/' + id)
}
export const useDictDataSubmitApi = (dataForm: any) => {
if (dataForm.id) {
return service.put('/sys/dict/data', dataForm)
} else {
return service.post('/sys/dict/data', dataForm)
}
}
import cache from '@/utils/cache'
import constant from '@/utils/constant'
// 文件导出
export const useLogLoginExportApi = () => {
location.href = constant.apiUrl + '/sys/log/login/export?access_token=' + cache.getToken()
}
import service from '@/utils/request'
export const useMenuNavApi = () => {
return service.get('/sys/menu/nav')
}
export const useAuthorityListApi = () => {
return service.get('/sys/menu/authority')
}
export const useMenuListApi = (type: Number, applicationCode: String) => {
// 菜单类型 0:菜单 1:按钮 2:接口
const menuType = type === 2 ? 2 : 0
return service.get('/sys/menu/list?type=' + menuType + '&applicationCode=' + applicationCode)
}
export const useMenuApi = (id: Number) => {
return service.get('/sys/menu/' + id)
}
export const useMenuSubmitApi = (dataForm: any) => {
if (dataForm.id) {
return service.put('/sys/menu', dataForm)
} else {
return service.post('/sys/menu', dataForm)
}
}
import service from '@/utils/request'
export const useOrgListApi = () => {
return service.get('/sys/org/list')
}
export const useOrgApi = (id: Number) => {
return service.get('/sys/org/' + id)
}
export const useOrgSubmitApi = (dataForm: any) => {
if (dataForm.id) {
return service.put('/sys/org', dataForm)
} else {
return service.post('/sys/org', dataForm)
}
}
import service from '@/utils/request'
export const useParamsApi = (id: number) => {
return service.get('/sys/params/' + id)
}
export const useParamsSubmitApi = (dataForm: any) => {
if (dataForm.id) {
return service.put('/sys/params', dataForm)
} else {
return service.post('/sys/params', dataForm)
}
}
import service from '@/utils/request'
export const usePostApi = (id: Number) => {
return service.get('/sys/post/' + id)
}
export const usePostSubmitApi = (dataForm: any) => {
if (dataForm.id) {
return service.put('/sys/post', dataForm)
} else {
return service.post('/sys/post', dataForm)
}
}
export const usePostListApi = () => {
return service.get('/sys/post/list')
}
import service from '@/utils/request'
export const useRoleMenuApi = () => {
return service.get('/sys/role/menu')
}
export const useRoleApi = (id: number) => {
return service.get('/sys/role/' + id)
}
export const useRoleListApi = () => {
return service.get('/sys/role/list')
}
export const useRoleSubmitApi = (dataForm: any) => {
if (dataForm.id) {
return service.put('/sys/role', dataForm)
} else {
return service.post('/sys/role', dataForm)
}
}
export const useRoleDataScopeSubmitApi = (dataForm: any) => {
return service.put('/sys/role/data-scope', dataForm)
}
export const useRoleUserSubmitApi = (roleId: number, dataForm: any) => {
return service.post('/sys/role/user/' + roleId, dataForm)
}
import service from '@/utils/request'
export const useUserInfoApi = () => {
return service.get('/sys/user/info')
}
export const updatePasswordApi = (data: any) => {
return service.put('/sys/user/password', data)
}
export const useUserApi = (id: number) => {
return service.get('/sys/user/' + id)
}
export const useUserSubmitApi = (dataForm: any) => {
if (dataForm.id) {
return service.put('/sys/user', dataForm)
} else {
return service.post('/sys/user', dataForm)
}
}
import service from '@/utils/request'
/**
* 获取几点信息
* @param flowId
* @returns {AxiosPromise}
*/
export const getHistoryActivityList = (flowId:any) => {
return service.post('/flow2/getHistoryActivityListAndNodes',{ flowId: flowId })
}
/**
* 提交审批
* @param flowId
* @param opinion
* @param comment
* @returns {AxiosPromise}
*/
export const complete = (flowId :any, opinion:any, comment:any) => {
return service.post('/flow/process/complete',{ flowId: flowId, opinion: opinion, comment: comment })
}
export const getCurrentNodeInfo = (flowId: any) => {
return service.post('/flow2/getCurrentNodeInfo', { flowId: flowId })
}
/**
* 转办
* @param flowId
* @param username
*/
export const transition = (flowId:any, username:any) => {
return service.post('/flow/transition', { flowId: flowId, username: username })
}
export const stop = (flowId:any, reason:any) => {
return service.post( '/flow/stop',{ flowId: flowId, reason: reason })
}
import Flow from './src/index.vue'
export { Flow }
<template>
<el-dialog
width="30%"
:title="
props.recommendation == 'aso_success' ? '同意': '驳回'
"
v-model="dialogShow"
@close="handleCloses"
>
<el-form :model="form">
<el-row>
<el-col :span="24">
<el-form-item label="操作类型">
</el-form-item>
</el-col>
<el-col :span="24">
<el-input
clearable
v-model="form.comment"
type="textarea"
:autosize="{ minRows: 4, maxRows: 8 }"
placeholder="审批意见"
/>
</el-col>
<el-col :span="24">
<div v-if="props.recommendation != 'aso_success'"> </div>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="submit"> 提交 </el-button>
<el-button @click="handleCloses">取消</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
const props = defineProps(['show', 'recommendation'])
const emit = defineEmits(['clone', 'submit', 'reject'])
let dialogShow = ref(false)
let form = ref({})
let QuickSuggestions = ref('')
watch(
() => props.show,
() => {
console.log(props.show + '1')
dialogShow.value = props.show
},
{ deep: true }
)
watch(
() => props.recommendation,
() => {
QuickSuggestions.value = props.recommendation
},
{ deep: true }
)
const handleCloses = () => {
emit('clone')
}
const changeComment = () => {
form.value.comment = form.value.option
}
const submit = () => {
console.log('22222222', props.recommendation)
if (props.recommendation == 'aso_success') {
emit('submit', form.value.comment)
form.value = {}
} else {
emit('reject', form.value.comment)
form.value = {}
}
}
</script>
<style scoped></style>
<template>
<div>
<el-card>
<!--流程审批-->
<div class="about">
<el-steps :active="data.lct.length" align-center finish-status="success">
<el-step
v-for="(item, index) in data.lct"
:key="index"
:title="item.activityName"
:description="item.assigneeName"
:status="statusArr[item.activityState]"
/>
</el-steps>
</div>
<el-row
v-show="
(data.isCurrentUser &&
flowStatus != 'REJECT' &&
flowStatus != 'CANCEL' &&
flowStatus != 'UN_SUBMIT' &&
flowStatus != '0' &&
flowStatus != '3' &&
flowStatus != '4') ||
zdShow
"
>
<el-col style="margin-bottom: 10px; margin-top: 5px">
<el-button
@click="agreeFun"
type="success"
v-show="
data.isCurrentUser &&
flowStatus != 'REJECT' &&
flowStatus != 'CANCEL' &&
flowStatus != 'UN_SUBMIT' &&
flowStatus != '0' &&
flowStatus != '3' &&
flowStatus != '4'
"
>同意</el-button
>
<el-button
type="danger"
@click="goReject"
v-show="
data.isCurrentUser &&
flowStatus != 'REJECT' &&
flowStatus != 'CANCEL' &&
flowStatus != 'UN_SUBMIT' &&
flowStatus != '0' &&
flowStatus != '3' &&
flowStatus != '4'
"
>驳回</el-button
>
<el-popconfirm
@confirm="stops"
confirm-button-text="确认"
cancel-button-text="取消"
title="是否确定要撤回?"
>
<template #reference>
<el-button
v-if="
zdShow &&
flowStatus != 'CANCEL' &&
flowStatus != 'UN_SUBMIT' &&
flowStatus != 'SUCCESS' &&
flowStatus != '0' &&
flowStatus != '2' &&
flowStatus != '4'
"
type="danger"
:loading="data.isLoading"
>撤回</el-button
>
</template>
</el-popconfirm>
</el-col>
</el-row>
<el-row>
<el-table :data="data.historyActivityList" :border="props.border">
<el-table-column prop="name" label="节点" />
<el-table-column prop="assigneeName" label="审批人" />
<el-table-column prop="type" label="处理类型" />
<el-table-column prop="startTime" label="开始时间" />
<el-table-column prop="endTime" label="结束时间" />
<el-table-column
prop="comment"
label="审批意见"
show-overflow-tooltip
>
</el-table-column>
</el-table>
</el-row>
<edit
:show="data.isShow"
:recommendation="data.Recommendation"
@clone="clone"
@submit="handleComplete"
@reject="handleReject"
/>
</el-card>
</div>
</template>
<script lang="ts" setup>
import {
complete,
getCurrentNodeInfo,
getHistoryActivityList,
transition,
stop,
} from '../api/flow/flow'
import { defineEmits, reactive, watch, ref } from 'vue'
import edit from './edit.vue'
const emit = defineEmits(['complete', 'goFlowAgree', 'stops', 'flowCanEdit'])
let form = ref({})
const props = defineProps({
flowId: Number,
flowStatus: {
type: String,
default: 'TO_CHECK'
},
flowCategory: String, //流程分类标题
customEvent: Boolean, // 自定义提交事件
passSendEmail: Object,
border: {
type: Boolean,
default: true
}
})
let statusArr = ref(['success', 'process', 'wart'])
let isShow = ref(false)
const data = reactive({
isLoading: false,
imgUrl: null,
isShow: false,
flowFormProperties: [],
Recommendation: '',
comment: '', // 审批意见
historyActivityList: [], // 历史审批列表
isCurrentUser: false,
lct: [],
validatorForm: [],
isAssignee: false, // 是否当前节点受理人
hasReject: false, // 是否有驳回操作
nodeName: '' // 当前审批节点
})
const zdShow = ref(false)
let dialogFormVisible = ref(false)
const getFlowImageF = () => {
}
async function onComplete(cb) {
try {
data.isLoading = true
await complete(props.flowId, true, data.comment)
// this.$message.success('操作成功')
cb && cb()
data.isLoading = false
} catch (error) {
data.isLoading = false
}
refresh()
}
async function agr() {
isShow.value = true
}
const agreeFun = () => {
emit('goFlowAgree')
}
const getUserId = (val) => {
form.value.username = val
}
const goReject = () => {
data.isShow = true
data.Recommendation = 'aso_reject'
}
async function handleComplete(val: string) {
data.isShow = false
if (props.customEvent) {
emit('complete')
} else {
data.isLoading = true
data.comment = val
complete(props.flowId, true, data.comment)
.then((response) => {
refresh()
emit('complete')
})
.finally(() => {
data.isLoading = false
})
}
}
const a = () => {
data.isShow = true
data.Recommendation = 'aso_success'
}
const handleTransition = () => {
// this.$refs.tableDialog.handleClick()
emit('complete')
dialogFormVisible.value = true
}
const handleReject = (val) => {
data.isShow = false
data.isLoading = true
data.comment = val
complete(props.flowId, 'false', data.comment)
.then((response) => {
refresh()
})
.finally(() => {
data.isLoading = false
})
}
//const { wsCache } = useCache()
const userInfo = {username:'guoxianhui'}//wsCache.get('userInfo')
const getHistoryActivityListF = () => {
getHistoryActivityList(props.flowId).then((response) => {
data.historyActivityList = response.data.nodeInfoList
/***
data.lct = response.data.flowNodeList
if (
data.lct[0].assignee == userInfo.username &&
response.data.flowNodeList[response.data.flowNodeList.length - 1].activityState != 3
) {
zdShow.value = true
} else {
zdShow.value = false
}
data.lct = data.lct.filter((item) => item.activityName != null)
console.log(data.lct)
***/
})
}
const getCurrentNodeInfoF = () => {
getCurrentNodeInfo(props.flowId).then((response) => {
// 是否当前节点受让人
data.isAssignee = response.data.isAssignee
data.hasReject = response.data.hasReject
data.nodeName = response.data.nodeName
data.isCurrentUser = false
if (data.isAssignee) {
data.isCurrentUser = true
}
if (data.hasReject) {
data.isCurrentUser = true
}
emit('flowCanEdit', data)
if (!data.isCurrentUser) {
emit('setRead')
}
})
}
const refresh=() =>{
debugger
if (!props.flowId || props.flowId == 0) {
data.historyActivityList = []
data.lct = []
data.isAssignee = false
return
}
getFlowImageF()
getHistoryActivityListF()
getCurrentNodeInfoF()
}
const clone = () => {
data.isShow = false
data.lct = []
}
const confirmAdmin = (row) => {
transition(props.flowId, form.value.username).then(() => {
dialogFormVisible.value = false
// this.$message.success('操作成功')
refresh()
})
}
/**
* 中断
*/
const stops = () => {
data.isLoading = true
stop(props.flowId, props.comment)
.then(() => {
refresh()
emit('stops')
})
.finally(() => {
data.isLoading = false
})
}
defineExpose({
refresh,a
})
</script>
<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<!-- eslint-disable vue/v-on-event-hyphenation -->
<template>
<div>
<el-row :gutter="20">
<el-col :span="4">
<el-card>
<el-input clearable v-model="data.name" />
<el-tree
ref="tree"
style="height: 35rem; overflow: auto"
class="filter-tree"
:data="data.treeData"
default-expand-all
:expand-on-click-node="false"
:filter-node-method="filterNode"
:props="{ children: 'children', label: 'name' }"
@node-click="handleNodeClick"
/>
</el-card>
</el-col>
<el-col :span="20">
<el-form ref="form" :model="data.form" label-width="100px">
<el-card header="用户列表">
<el-table
ref="listTable"
v-loading="data.loading"
:data="data.tableData"
:height="data.tableHeight"
@selection-change="jigou"
border
>
<el-table-column type="selection" width="40" />
<el-table-column prop="username" label="mip账号" show-overflow-tooltip />
<el-table-column prop="name" label="姓名" show-overflow-tooltip />
<!--
<el-table-column prop="postName" label="Post Name" show-overflow-tooltip />
<el-table-column label="Operation" width="200">
<template #default>
</template>
</el-table-column> -->
</el-table>
<el-row class="footer-box">
<!-- <el-col :span="12">-->
<!-- <el-button type="primary" @click="handleEdit(null);">新增</el-button>-->
<!-- </el-col>-->
<el-col :span="12">
<pagination
:total="data.pagination.total"
v-model:page="data.pagination.current"
v-model:limit="data.pagination.size"
@pagination="getPage"
/>
</el-col>
</el-row>
</el-card>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { treeList, getInfoByGroupId, saveOrUpdate } from '@/api/system/group'
import { reactive, ref } from 'vue'
import { isEmpty } from '@/utils/validate'
import { getUserListByGroupId, listPageApi } from '@/api/system/admin'
import { emit } from 'process'
const data = reactive({
name: '',
form: {},
treeData: [],
treeDatas: [],
tableData: [],
userLisData: [],
tableHeight: window.innerHeight - 385,
loading: false,
baseLoading: false,
// 分页
pagination: {
total: 0,
size: 20,
current: 1
},
condition: {
name: null,
id: null
},
// 编辑弹窗显示隐藏
showEdit: false,
// 新增数据
item: {},
// 部门负责人名称
userName: ''
})
const showEdit = ref(false)
// watch: {
// name(val) {
// this.$refs.tree.filter(val)
// }
// },
// created() {
// this.getListTreePage()
// },
const onSubmit = (formName) => {
data.baseLoading = true
saveOrUpdate(data.form).then(() => {
// this.$message.success('操作成功')
data.baseLoading = false
getListTreePage()
})
}
const handleNodeClick = (datas) => {
console.log(datas)
if (datas.managerId) {
datas.managerId = datas.managerId + ''
}
data.form = datas
data.condition.id = datas.id
listPageApi(
{},
{
total: 0,
size: 20000,
current: 1
}
).then((response) => {
const { current, total, size, records } = response.data
data.userLisData = records
if (data.form.managerId) {
data.form.managerId = data.form.managerId + ''
}
})
// 根据组织架构id获取用户信息
if (!isEmpty(datas.id)) {
getUserListByGroupId(datas.id, data.pagination).then((response) => {
const { current, total, size, records } = response.data
data.tableData = records
data.pagination.total = total
data.pagination.current = current
data.pagination.size = size
})
}
}
const filterNode = (value, data, node) => {
if (!value) return true
return findSearKey(node, value)
}
// 递归搜索父级是否包含关键字
const findSearKey = (node, key) => {
if (node.label.indexOf(key) !== -1) {
return true
} else {
if (node.parent.parent == null) {
return false
} else {
return findSearKey(node.parent, key)
}
}
}
const getListTreePage = () => {
treeList().then((res) => {
data.treeData = res.data
const digui = (data, arr = []) => {
data.map((i) => {
const { name: label, id: value, ...val } = i
if (Array.isArray(val.children) && val.children.length) {
val.children = digui(val.children)
}
arr.push({
...val,
value,
label
})
})
return arr
}
data.treeDatas = digui(data.treeData)
// console.log(treList)
})
}
getListTreePage()
// 显示新增界面
const handleEdit = (data) => {
console.log(data)
data.item = data
console.log(data.item, 'data.item')
showEdit.value = true
}
// 关闭新增界面
const closeEdit = () => {
showEdit.value = false
data.item = {}
getListTreePage()
}
const emit = defineEmits(['getPage'])
const jigou = (val) => {
console.log(val)
emit('getPage', val[0].username)
}
// 列表数据分页
const getPage = () => {}
// 上级菜单返回参数
const popoverHide = (value, data) => {
data.form.parentName = data.name
data.form.parent = value
}
/** 选中部门负责人 */
const userSelect = (data) => {
data.form.managerId = isEmpty(data) ? '' : data.id
data.userName = isEmpty(data) ? '' : data.name
}
</script>
<style scoped></style>
<template>
<el-dialog v-model="dialogTableVisible" title="提交审批">
<el-row v-if="tableList.main.length > 0">
<el-col :span="24">
<el-table ref="tableRef" :data="tableList.main" v-if="tableList.main.length > 0">
<el-table-column type="index" label="序号" width="80" />
<el-table-column property="nodeName" label="节点名称" width="200" />
<el-table-column property="processType" label="审批类型" width="200" />
<el-table-column property="handlerNames" label="审批人">
<template #default="{ row, $index }">
<span>{{ row.handlerNames }}</span>
<el-button
type="text"
v-if="!(row.nodeName == '开始' || row.nodeName == '结束')"
@click="handleSelectUser($index)"
>
选择
</el-button>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="申请原因" prop="auditNote" label-width="90">
<el-input v-model="auditNote" type="textarea" />
</el-form-item>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="submit">提交</el-button>
</span>
</template>
<FastTransferUser v-model="showFastTransferUserDialog" :selectList="flowUserConfigSelectList" @confirm="handleConfirmUser"></FastTransferUser>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import type { Ref } from 'vue'
import { ElMessage, ElTable } from 'element-plus'
const tableRef = ref<InstanceType<typeof ElTable>>()
let dialogTableVisible = ref(false)
let tableList = ref({ main: [] as any[], subFb: [] })
let auditNote = ref('')
// 定义需要暴露出去的方法
const tabsExpose: Object = {
open: (opt): void => {
tableList.value = { main: [], subFb: [] }
// 判断当前tab是否存在
dialogTableVisible.value = true
// debugger
tableList.value = opt.tableList
},
close: (): void => {
// 判断当前tab是否存在
dialogTableVisible.value = false
}
}
const showFastTransferUserDialog = ref(false);
const flowUserConfigIndex = ref() as Ref<number>;
const flowUserConfigSelectList = ref([]);
const handleSelectUser = (index: number) => {
flowUserConfigIndex.value = index;
const handlerNamesList = tableList.value.main[index].handlerNames ? tableList.value.main[index].handlerNames.split(';') : [];
const handlerIdsList = tableList.value.main[index].handlerIds ? tableList.value.main[index].handlerIds.split(';') : [];
flowUserConfigSelectList.value = handlerNamesList.map((subItem: any,subIndex: number) => {
return {
realName: subItem,
username: handlerIdsList[subIndex]
}
});
showFastTransferUserDialog.value = true;
}
const handleConfirmUser = (list: any[]) => {
tableList.value.main[flowUserConfigIndex.value].handlerNames = list.map(i => i.realName).join(';');
tableList.value.main[flowUserConfigIndex.value].handlerIds = list.map(i => i.username).join(';');
tableList.value.main[flowUserConfigIndex.value].changeHandlerIds = list.map(i => i.username).join(';');
}
const emit = defineEmits(['submit'])
const submit = () => {
for (var i = 0; i < tableList.value.main.length; i++) {
if (tableList.value.main[i].mustSelected && tableList.value.main[i].handlerIds.length == 0) {
ElMessage.warning(tableList.value.main[i].nodeName + '节点审批人不能为空!')
return
}
if (i > 0 && tableList.value.main[i].handlerIds.length > 0) {
tableList.value.main[i].changeHandlerIds = '111'
}
}
dialogTableVisible.value = false
emit('submit', tableList, auditNote)
}
defineExpose(tabsExpose)
</script>
import { withInstall } from '@/utils/tool'
import FastRadioGroup from './src/fast-radio-group.vue'
export default withInstall(FastRadioGroup)
<template>
<el-radio-group :model-value="modelValue + ''" @change="$emit('update:modelValue', $event)">
<el-radio v-for="data in dataList" :key="data.dictValue" :label="data.dictValue">{{ data.dictLabel }}</el-radio>
</el-radio-group>
</template>
<script setup lang="ts" name="FastRadioGroup">
import { useAppStore } from '@/store/modules/app'
import { getDictDataList } from '@/utils/tool'
const appStore = useAppStore()
const props = defineProps({
modelValue: {
type: [Number, String],
required: true
},
dictType: {
type: String,
required: true
}
})
const dataList = getDictDataList(appStore.dictList, props.dictType)
</script>
import { withInstall } from '@/utils/tool'
import FastSelect from './src/fast-select.vue'
export default withInstall(FastSelect)
<template>
<el-select
:model-value="modelValue"
:placeholder="placeholder"
:multiple="multiple"
:collapse-tags="multiple"
:clearable="clearable" style="width: 200px"
@change="$emit('update:modelValue', $event)"
>
<el-option v-for="data in dataList" :key="data.dictValue" :label="data.dictLabel" :value="data.dictValue">
{{ data.dictLabel }}
</el-option>
</el-select>
</template>
<script setup lang="ts" name="FastSelect">
import { getDictDataList } from '@/utils/tool'
import { useAppStore } from '@/store/modules/app'
const appStore = useAppStore()
const props = defineProps({
modelValue: {
type: [Number, String],
required: true
},
multiple: {
type: Boolean,
required: false,
default: () => false
},
dictType: {
type: String,
required: true
},
clearable: {
type: Boolean,
required: false,
default: () => false
},
placeholder: {
type: String,
required: false,
default: () => ''
}
})
const dataList = getDictDataList(appStore.dictList, props.dictType)
</script>
import { withInstall } from '@/utils/tool'
import FastTableColumn from './src/fast-table-column.vue'
export default withInstall(FastTableColumn)
<template>
<el-table-column
:prop="prop"
:label="label"
:header-align="headerAlign"
:align="align"
:width="width"
:min-width="minWidth"
:class-name="className"
>
<template #default="scope">
<span v-html="getDictLabelList(props.dictType, scope.row[props.prop])"></span>
</template>
</el-table-column>
</template>
<script setup lang="ts" name="FastTableColumn">
import { getDictLabelList } from '@/utils/tool'
const props = defineProps({
prop: {
type: String,
required: true
},
label: {
type: String,
required: true
},
dictType: {
type: String,
required: true
},
headerAlign: {
type: String,
required: false,
default: () => 'center'
},
align: {
type: String,
required: false,
default: () => 'center'
},
width: {
type: String,
required: false,
default: () => ''
},
minWidth: {
type: String,
required: false,
default: () => ''
},
className: {
type: String,
required: false,
default: () => ''
}
})
</script>
import { withInstall } from '@/utils/tool'
import FastTransferPost from './src/fast-transfer-post.vue'
export default withInstall(FastTransferPost)
<template>
<el-dialog v-model="showDialog" title="岗位选择" append-to-body top="5vh" :close-on-click-modal="false" width="1080px" draggable>
<div class="transfer-user-container">
<div class="user-container">
<div class="user-table-container">
<div class="user-table-header">岗位列表</div>
<el-form :inline="true" :model="queryForm" class="search-form">
<el-form-item>
<el-input v-model="queryForm.postCode" placeholder="岗位编码"></el-input>
</el-form-item>
<el-form-item>
<el-input v-model="queryForm.postName" placeholder="岗位名称"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getDataList">查询</el-button>
</el-form-item>
</el-form>
<el-table class="user-table" v-loading="dataListLoading" :data="dataList" height="100%" border style="width: 100%" @selection-change="selectionChangeHandle">
<el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column>
<el-table-column prop="postName" label="岗位名称" header-align="center" align="center"></el-table-column>
<el-table-column prop="postCode" label="岗位编码" header-align="center" align="center"></el-table-column>
<el-table-column prop="sort" label="排序" header-align="center" align="center"></el-table-column>
</el-table>
<el-pagination
style="margin: 10px;"
:current-page="page"
:page-sizes="pageSizes"
:page-size="limit"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
></el-pagination>
</div>
<div class="user-btns-container">
<el-button type="primary" @click="handleToLeft" :disabled="!currentSelectRight.length">
<el-icon><ArrowLeftBold /></el-icon>
</el-button>
<el-button type="primary" @click="handleToRight" :disabled="!dataListTableSelection!.length">
<el-icon><ArrowRightBold /></el-icon>
</el-button>
</div>
<div class="user-target-container">
<div class="user-target-header">选中列表(点击选中)</div>
<div class="user-target-list">
<div class="user-target-item" :class="{active: currentSelectRight.includes(item.postCode)}" v-for="(item,index) in rightList" :key="index" @click="handleClickRight(item)">{{ item.postName }}</div>
</div>
</div>
</div>
</div>
<template #footer>
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="handleSave">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="FastTransferPost">
import { computed,ref,watch,PropType } from 'vue';
import useFastTransferPost from './useFastTransferPost';
const props = defineProps({
modelValue: Boolean,
selectList: {
type: Array as PropType<typeof dataList.value>,
default: () => ([])
},
})
const emits = defineEmits(['update:modelValue','confirm']);
const showDialog = computed({
get: () => props.modelValue,
set: val => emits('update:modelValue',val)
})
const { getDataList,selectionChangeHandle,sizeChangeHandle,currentChangeHandle,queryForm,dataList,dataListLoading,page,pageSizes,limit,total,dataListTableSelection,resetData } = useFastTransferPost();
watch(showDialog,val => {
if(val) {
resetData();
currentChangeHandle(1);
rightList.value = props.selectList.map(i => {
return {
...i
}
});
currentSelectRight.value = [];
}
})
const rightList: typeof dataList = ref([]);
const handleToRight = () => {
const primaryKeyList = rightList.value.map(i => i.postCode);
rightList.value = dataListTableSelection!.value?.reduce((pre,cur) => {
if(!primaryKeyList.includes(cur.postCode)) pre.push(cur);
return pre;
},rightList.value);
}
const currentSelectRight = ref<string[]>([]);
const handleToLeft = () => {
rightList.value = rightList.value.reduce((pre,cur) => {
if(!currentSelectRight.value.includes(cur.postCode)) pre.push(cur);
return pre;
},[] as typeof dataList.value);
currentSelectRight.value = [];
}
const handleClickRight = (item: typeof dataList.value[number]) => {
const index = currentSelectRight.value.findIndex(i => i === item.postCode);
if(currentSelectRight.value.includes(item.postCode)) currentSelectRight.value.splice(index,1);
else currentSelectRight.value.push(item.postCode);
}
const handleSave = () => {
emits('confirm',rightList.value);
showDialog.value = false;
}
</script>
<style lang="scss" scoped>
.transfer-user-container {
display: flex;
flex-direction: column;
height: 75vh;
.user-container {
flex: 1;
display: flex;
overflow: auto;
.user-table-container {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
border: 1px solid #ebeef5;
box-sizing: border-box;
border-radius: 4px;
overflow: hidden;
.user-table-header {
height: 40px;
line-height: 40px;
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
padding: 0 10px;
}
.search-form {
padding: 10px 10px 0;
}
:deep(.el-form-item--default) {
margin-bottom: 10px;
}
.user-table {
flex: 1;
}
}
.user-btns-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100px;
:deep(.el-button+.el-button) {
margin-left: 0;
margin-top: 10px;
}
}
.user-target-container {
width: 200px;
display: flex;
flex-direction: column;
border: 1px solid #ebeef5;
box-sizing: border-box;
border-radius: 4px;
overflow: hidden;
.user-target-header {
height: 40px;
line-height: 40px;
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
padding: 0 10px;
}
.user-target-list {
flex: 1;
overflow-y: auto;
.user-target-item {
line-height: 40px;
padding: 0 10px;
border-top: 1px solid #ebeef5;
cursor: pointer;
&:first-child {
border-top: none;
}
&.active {
background: #ecf5ff;
color: #409eff;
}
}
}
}
}
}
</style>
\ No newline at end of file
import { reactive,toRefs } from 'vue'
import { QueryFomType,PostDataType } from '../types/fastTransferPost'
import { useCrud } from '@/hooks'
import { IHooksOptions } from '@/hooks/interface'
interface FastTransferPost extends IHooksOptions {
queryForm: QueryFomType;
dataList: PostDataType[]
}
export default () => {
const state: FastTransferPost = reactive({
createdIsNeed: false,
dataListUrl: '/sys/post/page',
queryForm: {
status: 1
},
dataList: []
})
const { getDataList,selectionChangeHandle,sizeChangeHandle,currentChangeHandle } = useCrud(state);
const { queryForm,dataList,dataListLoading,page,pageSizes,limit,total,dataListTableSelection } = toRefs(state);
const resetData = () => {
queryForm.value = {
status: 1
};
dataListTableSelection!.value = [];
page!.value = 1;
}
return {
getDataList,
selectionChangeHandle,
sizeChangeHandle,
currentChangeHandle,
queryForm,
dataList,
dataListLoading,
page,
pageSizes,
limit,
total,
dataListTableSelection,
resetData
}
}
\ No newline at end of file
export interface QueryFomType {
postCode?: string;
postName?: string;
status?: number;
}
export interface PostDataType {
postCode: string;
postName: string;
status: number;
[propName: string]: any;
}
\ No newline at end of file
import { withInstall } from '@/utils/tool'
import FastTransferRole from './src/fast-transfer-role.vue'
export default withInstall(FastTransferRole)
<template>
<el-dialog v-model="showDialog" title="角色选择" append-to-body top="5vh" :close-on-click-modal="false" width="1080px" draggable>
<div class="transfer-user-container">
<div class="user-container">
<div class="user-table-container">
<div class="user-table-header">角色列表</div>
<el-form :inline="true" :model="queryForm" class="search-form">
<el-form-item>
<el-input v-model="queryForm.name" placeholder="名称"></el-input>
</el-form-item>
<el-form-item>
<el-input v-model="queryForm.roleCode" placeholder="编码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getDataList">查询</el-button>
</el-form-item>
</el-form>
<el-table class="user-table" v-loading="dataListLoading" :data="dataList" height="100%" border style="width: 100%" @selection-change="selectionChangeHandle">
<el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column>
<el-table-column prop="name" label="名称" header-align="center" align="center"></el-table-column>
<el-table-column prop="roleCode" label="编码" header-align="center" align="center"></el-table-column>
<el-table-column prop="remark" label="备注" header-align="center" align="center"></el-table-column>
</el-table>
<el-pagination
style="margin: 10px;"
:current-page="page"
:page-sizes="pageSizes"
:page-size="limit"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
></el-pagination>
</div>
<div class="user-btns-container">
<el-button type="primary" @click="handleToLeft" :disabled="!currentSelectRight.length">
<el-icon><ArrowLeftBold /></el-icon>
</el-button>
<el-button type="primary" @click="handleToRight" :disabled="!dataListTableSelection!.length">
<el-icon><ArrowRightBold /></el-icon>
</el-button>
</div>
<div class="user-target-container">
<div class="user-target-header">选中列表(点击选中)</div>
<div class="user-target-list">
<div class="user-target-item" :class="{active: currentSelectRight.includes(item.roleCode)}" v-for="(item,index) in rightList" :key="index" @click="handleClickRight(item)">{{ item.name }}</div>
</div>
</div>
</div>
</div>
<template #footer>
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="handleSave">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="FastTransferRole">
import { computed,ref,watch,PropType } from 'vue';
import useFastTransferRole from './useFastTransferRole';
const props = defineProps({
modelValue: Boolean,
selectList: {
type: Array as PropType<typeof dataList.value>,
default: () => ([])
},
})
const emits = defineEmits(['update:modelValue','confirm']);
const showDialog = computed({
get: () => props.modelValue,
set: val => emits('update:modelValue',val)
})
const { getDataList,selectionChangeHandle,sizeChangeHandle,currentChangeHandle,queryForm,dataList,dataListLoading,page,pageSizes,limit,total,dataListTableSelection,resetData } = useFastTransferRole();
watch(showDialog,val => {
if(val) {
resetData();
currentChangeHandle(1);
rightList.value = props.selectList.map(i => {
return {
...i
}
});
currentSelectRight.value = [];
}
})
const rightList: typeof dataList = ref([]);
const handleToRight = () => {
const primaryKeyList = rightList.value.map(i => i.roleCode);
console.log(111,dataListTableSelection!.value)
rightList.value = dataListTableSelection!.value?.reduce((pre,cur) => {
if(!primaryKeyList.includes(cur.roleCode)) pre.push(cur);
return pre;
},rightList.value);
console.log(rightList.value)
}
const currentSelectRight = ref<string[]>([]);
const handleToLeft = () => {
rightList.value = rightList.value.reduce((pre,cur) => {
if(!currentSelectRight.value.includes(cur.roleCode)) pre.push(cur);
return pre;
},[] as typeof dataList.value);
currentSelectRight.value = [];
}
const handleClickRight = (item: typeof dataList.value[number]) => {
const index = currentSelectRight.value.findIndex(i => i === item.roleCode);
if(currentSelectRight.value.includes(item.roleCode)) currentSelectRight.value.splice(index,1);
else currentSelectRight.value.push(item.roleCode);
}
const handleSave = () => {
emits('confirm',rightList.value);
showDialog.value = false;
}
</script>
<style lang="scss" scoped>
.transfer-user-container {
display: flex;
flex-direction: column;
height: 75vh;
.user-container {
flex: 1;
display: flex;
overflow: auto;
.user-table-container {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
border: 1px solid #ebeef5;
box-sizing: border-box;
border-radius: 4px;
overflow: hidden;
.user-table-header {
height: 40px;
line-height: 40px;
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
padding: 0 10px;
}
.search-form {
padding: 10px 10px 0;
}
:deep(.el-form-item--default) {
margin-bottom: 10px;
}
.user-table {
flex: 1;
}
}
.user-btns-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100px;
:deep(.el-button+.el-button) {
margin-left: 0;
margin-top: 10px;
}
}
.user-target-container {
width: 200px;
display: flex;
flex-direction: column;
border: 1px solid #ebeef5;
box-sizing: border-box;
border-radius: 4px;
overflow: hidden;
.user-target-header {
height: 40px;
line-height: 40px;
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
padding: 0 10px;
}
.user-target-list {
flex: 1;
overflow-y: auto;
.user-target-item {
line-height: 40px;
padding: 0 10px;
border-top: 1px solid #ebeef5;
cursor: pointer;
&:first-child {
border-top: none;
}
&.active {
background: #ecf5ff;
color: #409eff;
}
}
}
}
}
}
</style>
\ No newline at end of file
import { reactive,toRefs } from 'vue'
import { QueryFomType,RoleDataType } from '../types/fastTransferRole'
import { useCrud } from '@/hooks'
import { IHooksOptions } from '@/hooks/interface'
interface FastTransferUser extends IHooksOptions {
queryForm: QueryFomType;
dataList: RoleDataType[]
}
export default () => {
const state: FastTransferUser = reactive({
createdIsNeed: false,
dataListUrl: '/sys/role/page',
queryForm: {},
dataList: []
})
const { getDataList,selectionChangeHandle,sizeChangeHandle,currentChangeHandle } = useCrud(state);
const { queryForm,dataList,dataListLoading,page,pageSizes,limit,total,dataListTableSelection } = toRefs(state);
const resetData = () => {
queryForm.value = {};
dataListTableSelection!.value = [];
page!.value = 1;
}
return {
getDataList,
selectionChangeHandle,
sizeChangeHandle,
currentChangeHandle,
queryForm,
dataList,
dataListLoading,
page,
pageSizes,
limit,
total,
dataListTableSelection,
resetData
}
}
\ No newline at end of file
export interface QueryFomType {
name?: string;
roleCode?: string;
}
export interface RoleDataType {
name: string;
roleCode: string;
remark: string;
createTime: string;
[propName: string]: any;
}
\ No newline at end of file
import { withInstall } from '@/utils/tool'
import FastTransferUser from './src/fast-transfer-user.vue'
export default withInstall(FastTransferUser)
<template>
<el-dialog v-model="showDialog" title="人员选择" append-to-body top="5vh" :close-on-click-modal="false" width="1080px" draggable>
<div class="transfer-user-container">
<div class="user-container">
<div class="user-table-container">
<div class="user-table-header">用户列表</div>
<el-form :inline="true" :model="queryForm" class="search-form">
<el-form-item>
<el-input v-model="queryForm.username" placeholder="用户名"></el-input>
</el-form-item>
<el-form-item>
<el-input v-model="queryForm.mobile" placeholder="手机号"></el-input>
</el-form-item>
<el-form-item>
<fast-select v-model="queryForm.gender" dict-type="user_gender" clearable placeholder="性别"></fast-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getDataList">查询</el-button>
</el-form-item>
</el-form>
<el-table class="user-table" v-loading="dataListLoading" :data="dataList" height="100%" border style="width: 100%" @selection-change="selectionChangeHandle">
<el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column>
<el-table-column prop="username" label="用户名" header-align="center" align="center"></el-table-column>
<el-table-column prop="mobile" label="手机号" header-align="center" align="center"></el-table-column>
<el-table-column prop="realName" label="姓名" header-align="center" align="center"></el-table-column>
<fast-table-column prop="gender" label="性别" dict-type="user_gender"></fast-table-column>
</el-table>
<el-pagination
style="margin: 10px;"
:current-page="page"
:page-sizes="pageSizes"
:page-size="limit"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
></el-pagination>
</div>
<div class="user-btns-container">
<el-button type="primary" @click="handleToLeft" :disabled="!currentSelectRight.length">
<el-icon><ArrowLeftBold /></el-icon>
</el-button>
<el-button type="primary" @click="handleToRight" :disabled="!dataListTableSelection!.length">
<el-icon><ArrowRightBold /></el-icon>
</el-button>
</div>
<div class="user-target-container">
<div class="user-target-header">选中列表(点击选中)</div>
<div class="user-target-list">
<div class="user-target-item" :class="{active: currentSelectRight.includes(item.username as string)}" v-for="(item,index) in rightList" :key="index" @click="handleClickRight(item)">{{ item.realName }}</div>
</div>
</div>
</div>
</div>
<template #footer>
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="handleSave">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="FastTransferUser">
import { computed,ref,watch,PropType } from 'vue';
import useFastTransferUser from './useFastTransferUser';
const props = defineProps({
modelValue: Boolean,
selectList: {
type: Array as PropType<typeof dataList.value>,
default: () => ([])
},
})
const emits = defineEmits(['update:modelValue','confirm']);
const showDialog = computed({
get: () => props.modelValue,
set: val => emits('update:modelValue',val)
})
const { getDataList,selectionChangeHandle,sizeChangeHandle,currentChangeHandle,queryForm,dataList,dataListLoading,page,pageSizes,limit,total,dataListTableSelection,resetData } = useFastTransferUser();
watch(showDialog,val => {
if(val) {
resetData();
currentChangeHandle(1);
rightList.value = props.selectList.map(i => {
return {
...i
}
});
currentSelectRight.value = [];
}
})
const rightList: typeof dataList = ref([]);
const handleToRight = () => {
const primaryKeyList = rightList.value.map(i => i.username);
rightList.value = dataListTableSelection!.value?.reduce((pre,cur) => {
if(!primaryKeyList.includes(cur.username)) pre.push(cur);
return pre;
},rightList.value);
}
const currentSelectRight = ref<string[]>([]);
const handleToLeft = () => {
rightList.value = rightList.value.reduce((pre,cur) => {
if(!currentSelectRight.value.includes(cur.username as string)) pre.push(cur);
return pre;
},[] as typeof dataList.value);
currentSelectRight.value = [];
}
const handleClickRight = (item: typeof dataList.value[number]) => {
const index = currentSelectRight.value.findIndex(i => i === item.username)
if(currentSelectRight.value.includes(item.username as string)) currentSelectRight.value.splice(index,1);
else currentSelectRight.value.push(item.username as string);
}
const handleSave = () => {
emits('confirm',rightList.value);
showDialog.value = false;
}
</script>
<style lang="scss" scoped>
.transfer-user-container {
display: flex;
flex-direction: column;
height: 75vh;
.user-container {
flex: 1;
display: flex;
overflow: auto;
.user-table-container {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
border: 1px solid #ebeef5;
box-sizing: border-box;
border-radius: 4px;
overflow: hidden;
.user-table-header {
height: 40px;
line-height: 40px;
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
padding: 0 10px;
}
.search-form {
padding: 10px 10px 0;
}
:deep(.el-form-item--default) {
margin-bottom: 10px;
}
.user-table {
flex: 1;
}
}
.user-btns-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100px;
:deep(.el-button+.el-button) {
margin-left: 0;
margin-top: 10px;
}
}
.user-target-container {
width: 200px;
display: flex;
flex-direction: column;
border: 1px solid #ebeef5;
box-sizing: border-box;
border-radius: 4px;
overflow: hidden;
.user-target-header {
height: 40px;
line-height: 40px;
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
padding: 0 10px;
}
.user-target-list {
flex: 1;
overflow-y: auto;
.user-target-item {
line-height: 40px;
padding: 0 10px;
border-top: 1px solid #ebeef5;
cursor: pointer;
&:first-child {
border-top: none;
}
&.active {
background: #ecf5ff;
color: #409eff;
}
}
}
}
}
}
</style>
\ No newline at end of file
import { reactive,toRefs } from 'vue'
import { QueryFomType,UserDataType } from '../types/fastTransferUser'
import { useCrud } from '@/hooks'
import { IHooksOptions } from '@/hooks/interface'
interface FastTransferUser extends IHooksOptions {
queryForm: QueryFomType;
dataList: UserDataType[]
}
export default () => {
const state: FastTransferUser = reactive({
createdIsNeed: false,
dataListUrl: '/sys/user/page',
queryForm: {
gender: ''
},
dataList: [],
})
const { getDataList,selectionChangeHandle,sizeChangeHandle,currentChangeHandle } = useCrud(state);
const { queryForm,dataList,dataListLoading,page,pageSizes,limit,total,dataListTableSelection } = toRefs(state);
const resetData = () => {
queryForm.value = {
gender: ''
};
dataListTableSelection!.value = [];
page!.value = 1;
}
return {
getDataList,
selectionChangeHandle,
sizeChangeHandle,
currentChangeHandle,
queryForm,
dataList,
dataListLoading,
page,
pageSizes,
limit,
total,
dataListTableSelection,
resetData
}
}
\ No newline at end of file
export interface QueryFomType {
username?: string;
mobile?: string;
gender?: string;
}
export interface UserDataType {
username?: string;
mobile?: string;
realName?: string;
gender?: string;
createTime?: string;
[propName: string]: any;
}
\ No newline at end of file
import { withInstall } from '@/utils/tool'
import FastUser from './src/fast-user.vue'
export default withInstall(FastUser)
<template>
<div class="fast-user">
<el-form-item>
<el-button type="primary" @click="visible = true">新增</el-button>
</el-form-item>
<el-dialog v-model="visible" title="选择用户" :close-on-click-modal="false" :width="800" draggable>
<el-form :inline="true" :model="state.queryForm">
<el-form-item>
<el-input v-model="state.queryForm.username" placeholder="用户名"></el-input>
</el-form-item>
<el-form-item>
<el-input v-model="state.queryForm.mobile" placeholder="手机号"></el-input>
</el-form-item>
<el-form-item>
<fast-select v-model="state.queryForm.gender" dict-type="user_gender" clearable placeholder="性别"></fast-select>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
</el-form-item>
</el-form>
<el-table v-loading="state.dataListLoading" :data="state.dataList" border style="width: 100%" @selection-change="selectionChangeHandle">
<el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column>
<el-table-column prop="username" label="用户名" header-align="center" align="center"></el-table-column>
<el-table-column prop="mobile" label="手机号" header-align="center" align="center"></el-table-column>
<el-table-column prop="realName" label="姓名" header-align="center" align="center"></el-table-column>
<fast-table-column prop="gender" label="性别" dict-type="user_gender"></fast-table-column>
<el-table-column prop="createTime" label="创建时间" header-align="center" align="center" width="180"></el-table-column>
</el-table>
<el-pagination
:current-page="state.page"
:page-sizes="state.pageSizes"
:page-size="state.limit"
:total="state.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
>
</el-pagination>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="submitHandle()">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="FastUser">
import { reactive, ref } from 'vue'
import { IHooksOptions } from '@/hooks/interface'
import { useCrud } from '@/hooks'
import { ElMessage } from 'element-plus'
const state: IHooksOptions = reactive({
dataListUrl: '/sys/user/page',
queryForm: {
username: '',
mobile: '',
gender: ''
}
})
const visible = ref(false)
const emit = defineEmits(['select'])
const submitHandle = () => {
const dataList = state.dataListSelections ? state.dataListSelections : []
if (dataList.length === 0) {
ElMessage.warning('请选择用户记录')
return
}
visible.value = false
emit('select', dataList)
}
const { getDataList, selectionChangeHandle, sizeChangeHandle, currentChangeHandle } = useCrud(state)
</script>
<style lang="scss" scoped>
.fast-user {
display: inline-block;
}
</style>
<template>
<el-dropdown trigger="click" @command="languageChange">
<svg-icon icon="icon-translate"></svg-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="language in languages" :key="language" :disabled="locale === language" :command="language">
{{ messages[language].langName }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { messages } from '@/i18n'
import { useAppStore } from '@/store/modules/app'
const appStore = useAppStore()
const languages = Object.keys(messages)
const { locale } = useI18n()
const languageChange = (language: string) => {
appStore.setLanguage(language)
locale.value = language
// 刷新页面
window.location.reload()
}
</script>
<template>
<div>
<v-md-editor :model-value="modelValue" :height="height" @change="handleChange"></v-md-editor>
</div>
</template>
<script lang="ts" setup>
import VMdEditor from '@kangc/v-md-editor'
import githubTheme from '@kangc/v-md-editor/lib/theme/github.js'
import '@kangc/v-md-editor/lib/style/base-editor.css'
import '@kangc/v-md-editor/lib/theme/style/github.css'
// highlight
import hljs from 'highlight.js'
VMdEditor.use(githubTheme, {
Hljs: hljs
})
const props = defineProps({
modelValue: {
type: String,
required: true
},
height: {
type: String,
default: '400px'
}
})
// 编辑器change事件触发
const emit = defineEmits(['update:modelValue'])
const handleChange = (text: string, html: string) => {
emit('update:modelValue', text)
}
</script>
import { withInstall } from '@/utils/tool'
import SvgIcon from './src/svg-icon.vue'
export default withInstall(SvgIcon)
<template>
<div class="svg-icon">
<svg :class="`${className}`" :style="`color:${color};width: ${size};height: ${size}`" aria-hidden="true">
<use :xlink:href="iconName" />
</svg>
</div>
</template>
<script setup lang="ts" name="SvgIcon">
import { computed } from 'vue'
const props = defineProps({
icon: {
type: String,
required: true
},
color: {
type: String,
default: ''
},
className: {
type: String,
default: ''
},
size: {
type: String,
default: ''
}
})
// https://www.iconfont.cn 图标库需使用前缀 icon- 才能匹配
const iconName = computed(() => `#icon-${props.icon.replace('icon-', '')}`)
</script>
<style lang="scss" scoped>
.svg-icon {
display: inline-block;
svg {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
flex-shrink: 0;
}
}
</style>
<template>
<div style="border: 1px solid #ccc; z-index: 100">
<!-- 工具栏 -->
<Toolbar :editor="editorRef" :mode="mode" style="border-bottom: 1px solid #ccc" />
<!-- 编辑器 -->
<Editor
:model-value="modelValue"
:style="style"
:disabled="disabled"
:default-config="editorConfig"
:mode="mode"
@on-created="handleCreated"
@on-change="handleChange"
/>
</div>
</template>
<script lang="ts" setup>
import '@wangeditor/editor/dist/css/style.css'
import { onBeforeUnmount, shallowRef } from 'vue'
import constant from '@/utils/constant'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { IDomEditor, IEditorConfig } from '@wangeditor/editor'
import cache from '@/utils/cache'
const props = defineProps({
modelValue: {
type: String,
required: true
},
mode: {
type: String,
default: 'default' // 可选值:[default | simple]
},
placeholder: {
type: String,
default: ''
},
style: {
type: String,
default: 'height: 400px;'
},
disabled: {
type: Boolean,
default: false
}
})
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef()
type InsertFnType = (url: string, alt: string, href: string) => void
// 编辑器配置
const editorConfig: Partial<IEditorConfig> = {
placeholder: props.placeholder,
readOnly: props.disabled,
MENU_CONF: {
uploadImage: {
server: constant.uploadUrl + '?access_token=' + cache.getToken(),
fieldName: 'file',
// 自定义插入图片
customInsert(res: any, insertFn: InsertFnType) {
// res 即服务端的返回结果
// 从 res 中找到 url alt href ,然后插图图片
insertFn(res.data.url, res.data.name, '')
}
}
}
}
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) {
return
}
editor.destroy()
})
const handleCreated = (editor: IDomEditor) => {
editorRef.value = editor
}
// 编辑器change事件触发
const emit = defineEmits(['update:modelValue'])
const handleChange = (editor: IDomEditor) => {
emit('update:modelValue', editor.getHtml())
}
</script>
import { IHooksOptions } from '@/hooks/interface'
import service from '@/utils/request'
import { onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import qs from 'qs'
export const useCrud = (options: IHooksOptions) => {
const defaultOptions: IHooksOptions = {
createdIsNeed: true, //是否开启默认查询
dataListUrl: '',
queryMethod: 'get', //查询请求方式默认:get
isPage: true,
deleteUrl: '',
primaryKey: 'id',
exportUrl: '',
queryForm: {},
dataList: [],
order: '',
asc: false,
page: 1,
limit: 10,
total: 0,
pageSizes: [10, 20, 50, 100, 200],
dataListLoading: false,
dataListSelections: [],
dataListTableSelection: []
}
const mergeDefaultOptions = (options: any, props: any): IHooksOptions => {
for (const key in options) {
if (!Object.getOwnPropertyDescriptor(props, key)) {
props[key] = options[key]
}
}
return props
}
// 覆盖默认值
const state = mergeDefaultOptions(defaultOptions, options)
onMounted(() => {
if (state.createdIsNeed) {
query()
}
})
const query = () => {
if (!state.dataListUrl) {
return
}
state.dataListLoading = true
if (state.queryMethod == 'get') {
service
.get(state.dataListUrl, {
params: {
order: state.order,
asc: state.asc,
page: state.isPage ? state.page : null,
limit: state.isPage ? state.limit : null,
...state.queryForm
},
paramsSerializer: params => {
return qs.stringify(params)
}
})
.then((res: any) => {
state.dataList = state.isPage ? res.data.list : res.data
state.total = state.isPage ? res.data.total : 0
})
.finally(() => {
state.dataListLoading = false
})
} else {
service
.post(
state.dataListUrl,
{
order: state.order,
asc: state.asc,
page: state.isPage ? state.page : null,
limit: state.isPage ? state.limit : null,
...state.queryForm
},
{
paramsSerializer: params => {
return qs.stringify(params)
}
}
)
.then((res: any) => {
state.dataList = state.isPage ? res.data.list : res.data
state.total = state.isPage ? res.data.total : 0
})
.finally(() => {
state.dataListLoading = false
})
}
}
const getDataList = () => {
state.page = 1
query()
}
const sizeChangeHandle = (val: number) => {
state.page = 1
state.limit = val
query()
}
const currentChangeHandle = (val: number) => {
state.page = val
query()
}
// 多选
const selectionChangeHandle = (selections: any[]) => {
state.dataListTableSelection = JSON.parse(JSON.stringify(selections));
state.dataListSelections = selections.map((item: any) => state.primaryKey && item[state.primaryKey])
}
// 排序
const sortChangeHandle = (data: any) => {
const { prop, order } = data
if (prop && order) {
state.order = prop
state.asc = order === 'ascending'
} else {
state.order = ''
}
query()
}
const deleteHandle = (key: number | string) => {
if (!state.deleteUrl) {
return
}
ElMessageBox.confirm('确定进行删除操作?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
service.delete(state.deleteUrl + '/' + key).then(() => {
ElMessage.success('删除成功')
query()
})
})
.catch(() => {})
}
const deleteBatchHandle = (key?: number | string) => {
let data: any[] = []
if (key) {
data = [key]
} else {
data = state.dataListSelections ? state.dataListSelections : []
if (data.length === 0) {
ElMessage.warning('请选择删除记录')
return
}
}
ElMessageBox.confirm('确定进行删除操作?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
if (state.deleteUrl) {
service.delete(state.deleteUrl, { data }).then(() => {
ElMessage.success('删除成功')
query()
})
}
})
.catch(() => {})
}
const downloadHandle = async (url: string, params: object, method: string = 'GET', filename?: string): Promise<any> => {
try {
let res = null
if (method == 'POST') {
res = await service.post(url, params, {
responseType: 'blob'
})
} else {
res = await service({
responseType: 'blob',
url: url,
method: method
})
}
// 创建a标签
const down = document.createElement('a')
debugger
// 文件名没传,则使用时间戳
if (filename) {
down.download = filename
} else {
const downName = res.headers['content-disposition'].split('=')[1]
down.download = decodeURI(downName)
}
// 隐藏a标签
down.style.display = 'none'
// 创建下载url
down.href = URL.createObjectURL(
new Blob([res.data], {
type: res.data.type
})
)
// 模拟点击下载
document.body.appendChild(down)
down.click()
// 释放URL
URL.revokeObjectURL(down.href)
// 下载完成移除
document.body.removeChild(down)
} catch (err: any) {
ElMessage.error(err.message)
}
}
return {
getDataList,
sizeChangeHandle,
currentChangeHandle,
selectionChangeHandle,
sortChangeHandle,
deleteHandle,
deleteBatchHandle,
downloadHandle
}
}
export interface IHooksOptions {
// 是否在创建页面时,调用数据列表接口
createdIsNeed?: boolean
// 数据列表 Url
dataListUrl?: string
// 请求方式
queryMethod?: string
// 是否需要分页
isPage?: boolean
// 删除 Url
deleteUrl?: string
// 主键key,用于删除场景
primaryKey?: string
// 导出 Url
exportUrl?: string
// 查询条件
queryForm?: any
// 数据列表
dataList?: any[]
// 排序字段
order?: string
// 是否升序
asc?: boolean
// 当前页码
page?: number
// 每页数
limit?: number
// 总条数
total?: number
pageSizes?: any[]
// 数据列表,loading状态
dataListLoading?: boolean
// 数据列表,多选项
dataListSelections?: any[]
// 数据列表,表格选中row[]
dataListTableSelection?: any[]
}
import { createI18n } from 'vue-i18n'
import cache from '@/utils/cache'
// element-plus 国际化文件
import element_zh_cn from 'element-plus/es/locale/lang/zh-cn'
import element_en from 'element-plus/es/locale/lang/en'
// 框架 国际化文件
const zh_cn = import.meta.globEager('./lang/**/zh-CN.ts')
const en_us = import.meta.globEager('./lang/**/en-US.ts')
// 加载 lang 文件夹下的国际化
export const loadLang = (modules: Record<string, any>) => {
const messages: { [key: string]: string } = {}
Object.keys(modules).forEach(module => {
Object.assign(messages, { ...modules[module].default })
})
return messages
}
export const messages: { [key: string]: any } = {
'zh-CN': {
langName: '简体中文',
...loadLang(zh_cn),
el: {
...element_zh_cn
}
},
'en-US': {
langName: 'English',
...loadLang(en_us),
el: {
...element_en
}
}
}
// 返回当前 Language
function getLanguage() {
return cache.getLanguage()
}
export const i18n = createI18n({
locale: getLanguage(),
messages
})
export default {
loading: 'Loading...',
add: 'Add',
delete: 'Delete',
edit: 'Edit',
query: 'Query',
export: 'Export',
handle: 'Action',
back: 'Back',
confirm: 'Confirm',
cancel: 'Cancel',
clear: 'Clear',
close: 'Close',
createTime: 'Create Time',
updateTime: 'Update Time',
required: 'Required items cannot be empty',
app: {
title: 'Admin',
description: '',
logoText: 'OMS',
miniLogoText: 'OMS',
username: 'Username',
password: 'Password',
captcha: 'Captcha',
accountSignIn: 'Account Sign in',
mobileSignIn: 'Mobile Sign in',
mobile: 'Mobile',
signIn: 'Sign in',
signOut: 'Sign Out',
large: 'Large',
default: 'Default',
small: 'Small',
close: 'Close',
closeOthers: 'Close Others',
closeAll: 'Close All'
},
settings: {
title: 'Layout Settings',
sidebarDark: 'Dark Sidebar',
sidebarLight: 'Light Sidebar',
navbarLight: 'Light Navbar',
navbarTheme: 'Theme Navbar',
layout: 'Layout Switch',
vertical: 'Vertical',
columns: 'Columns',
transverse: 'Transverse',
interface: 'Interface Settings',
uniqueOpened: 'Unique Opened',
dark: 'Enable Dark',
logo: 'Enable Logo',
breadcrumb: 'Enable Breadcrumb',
tabs: 'Enable Tabs',
tabsCache: 'Enable Tabs Cache',
tabsStyle: 'Tabs Style',
tips: 'After setting, it will only take effect temporarily. To take effect permanently, you need to click the "Copy Config" button below to replace the configuration in store/theme/config.ts. ',
copyConfig: 'Copy Config',
reset: 'Reset',
copySuc: 'Copy Succeeded',
style1: 'Style-1',
style2: 'Style2'
},
error: {
email: 'The email format is incorrect',
password: 'The password cannot be less than {len} digits'
},
router: {
home: 'Home',
profilePassword: 'Change Password'
}
}
export default {
profile: {
username: 'Username',
oldPassword: 'Original Password',
newPassword: 'New Password',
confirmPassword: 'Confirm Password'
}
}
export default {
profile: {
username: '用户名',
oldPassword: '原密码',
newPassword: '新密码',
confirmPassword: '确认密码'
}
}
export default {
loading: '加载中...',
add: '新增',
delete: '删除',
edit: '修改',
query: '查询',
export: '导出',
handle: '操作',
back: '返回',
confirm: '确定',
cancel: '取消',
clear: '清除',
close: '关闭',
createTime: '创建时间',
updateTime: '更新时间',
required: '必填项不能为空',
app: {
title: '鹏辉备件系统',
description: '',
logoText: 'SPMS',
miniLogoText: 'SPMS',
username: '用户名',
password: '密码',
captcha: '验证码',
signIn: '登录',
accountSignIn: '账号密码登录',
mobileSignIn: '手机短信登录',
mobile: '手机号',
signOut: '退出',
large: '大型',
default: '默认',
small: '小型',
close: '关闭',
closeOthers: '关闭其它',
closeAll: '关闭全部'
},
settings: {
title: '布局设置',
sidebarDark: '暗色侧边栏',
sidebarLight: '亮色侧边栏',
navbarLight: '亮色顶栏',
navbarTheme: '主题色顶栏',
layout: '布局切换',
vertical: '纵向',
columns: '分栏',
transverse: '横向',
interface: '界面设置',
dark: '开启暗黑模式',
uniqueOpened: '侧栏排他展开',
logo: '开启LOGO',
breadcrumb: '开启面包屑',
tabs: '开启标签页',
tabsCache: '开启标签页缓存',
tabsStyle: '标签页风格',
tips: '设置之后仅是临时生效,要想永久生效,需点击下方的 "复制配置" 按钮,将配置替换到 store/theme/config.ts 中。',
copyConfig: '复制配置',
reset: '恢复默认',
copySuc: '复制成功',
style1: '风格1',
style2: '风格2'
},
error: {
email: '邮箱格式不正确',
password: '密码不能小于{len}位数'
},
router: {
home: '首页',
profilePassword: '修改密码'
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M121.718 73.272v9.953c3.957-7.584 6.199-16.05 6.199-24.995C127.917 26.079 99.273 0 63.958 0 28.644 0 0 26.079 0 58.23c0 .403.028.806.028 1.21l22.97-25.953h13.34l-19.76 27.187h6.42V53.77l13.728-19.477v49.361H22.998V73.272H2.158c5.951 20.284 23.608 36.208 45.998 41.399-1.44 3.3-5.618 11.263-12.565 12.674-8.607 1.764 23.358.428 46.163-13.178 17.519-4.611 31.938-15.849 39.77-30.513h-13.506V73.272H85.02V59.464l22.998-25.977h13.008l-19.429 27.187h6.421v-7.433l13.727-19.402v39.433h-.027zm-78.24 2.822a10.516 10.516 0 0 1-.996-4.535V44.548c0-1.613.332-3.124.996-4.535a11.66 11.66 0 0 1 2.713-3.68c1.134-1.032 2.49-1.864 4.04-2.468 1.55-.605 3.21-.908 4.982-.908h11.292c1.77 0 3.431.303 4.981.908 1.522.604 2.85 1.41 3.986 2.418l-12.26 16.303v-2.898a1.96 1.96 0 0 0-.665-1.512c-.443-.403-.996-.604-1.66-.604-.665 0-1.218.201-1.661.604a1.96 1.96 0 0 0-.664 1.512v9.071L44.364 77.606a10.556 10.556 0 0 1-.886-1.512zm35.73-4.535c0 1.613-.332 3.124-.997 4.535a11.66 11.66 0 0 1-2.712 3.68c-1.134 1.032-2.49 1.864-4.04 2.469-1.55.604-3.21.907-4.982.907H55.185c-1.77 0-3.431-.303-4.981-.907-1.55-.605-2.906-1.437-4.041-2.47a12.49 12.49 0 0 1-1.384-1.512l13.727-18.217v6.375c0 .605.222 1.109.665 1.512.442.403.996.604 1.66.604.664 0 1.218-.201 1.66-.604a1.96 1.96 0 0 0 .665-1.512V53.87L75.97 36.838c.913.932 1.66 1.99 2.214 3.175.664 1.41.996 2.922.996 4.535v27.011h.028z"/></svg>
\ No newline at end of file
<template>
<div v-if="appStore.sidebarOpened" class="sidebar-logo">
<el-image class="image" src="./favicon.ico"></el-image>
<span class="logo-title"> {{ $t('app.logoText') }}</span>
</div>
<div v-else class="sidebar-logo sidebar-logo-expend">
<span>{{ $t('app.miniLogoText') }}</span>
</div>
</template>
<script setup lang="ts">
import {useAppStore} from '@/store/modules/app'
const appStore = useAppStore()
</script>
<style lang="scss" scoped>
.sidebar-logo {
width: 230px !important;
height: var(--theme-header-height);
line-height: var(--theme-header-height);
display: flex;
align-items: center;
justify-content: center;
box-shadow: rgb(0 21 41 / 2%) 0 1px 4px;
color: var(--theme-logo-text-color);
font-size: 18px;
.image {
width: 146px;
height: 50px;
}
.logo-title {
margin-left: 10px;
}
}
.sidebar-logo-expend {
width: 100% !important;
}
</style>
<template>
<el-main class="layout-main">
<div class="layout-body">
<div class="layout-card">
<!--<router-view v-slot="{ Component, route }">
<keep-alive v-if="theme.isTabsCache" :include="[...tabsStore.cachedViews]">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
<component :is="Component" v-else :key="route.name" />
</router-view> -->
<router-view v-slot="{ Component, route }">
<keep-alive>
<component :is="Component" :key="route.path" class="main-body-container" />
</keep-alive>
</router-view>
</div>
</div>
</el-main>
</template>
<script setup lang="ts">
import { RouterView } from 'vue-router'
import { computed } from 'vue'
import { useAppStore } from '@/store/modules/app'
import { useTabsStore } from '@/store/modules/tabs'
const appStore = useAppStore()
const tabsStore = useTabsStore()
const theme = computed(() => appStore.theme)
</script>
<template>
<el-sub-menu v-if="menu.children.length > 0" :key="menu.path" :index="menu.path">
<template #title>
<svg-icon v-if="showIcon" :icon="menu.meta.icon"></svg-icon>
<span>{{ menu.meta.title }}</span>
</template>
<menu-item v-for="sub in menu.children" :key="sub.path" :menu="sub"></menu-item>
</el-sub-menu>
<el-menu-item v-else :key="menu.path" :index="menu.path" @click="handleClickMenu(menu)">
<svg-icon v-if="showIcon" :icon="menu.meta.icon"></svg-icon>
<template #title>
{{ menu.meta.title }}
</template>
</el-menu-item>
</template>
<script setup lang="ts">
import {computed, PropType} from 'vue'
import {useRouter} from 'vue-router'
import {isExternalLink, replaceLinkParam} from '@/utils/tool'
import {useAppStore} from '@/store/modules/app'
const appStore = useAppStore()
// 显示icon图标
const showIcon = computed(() => {
return appStore.theme.layout !== 'columns'
})
defineProps({
menu: {
type: Object as PropType<any>,
required: true
}
})
const router = useRouter()
// 菜单点击事件
const handleClickMenu = (menu: any) => {
// 不是新开页面,则直接切换路由
if (!menu.meta.newOpen) {
router.push(menu.path)
return
}
// 新开页面逻辑
if (isExternalLink(menu.meta.url)) {
// 外链
window.open(replaceLinkParam(menu.meta.url), '_blank')
} else {
// 内部组件
window.open('#' + menu.meta.url, '_blank')
}
}
</script>
<template>
<div class="navbar-left">
<Hamburger />
<Refresh />
<Breadcrumb v-if="appStore.theme.isBreadcrumb" />
</div>
</template>
<script setup lang="ts">
import Hamburger from './components/Hamburger.vue'
import Refresh from './components/Refresh.vue'
import Breadcrumb from './components/Breadcrumb.vue'
import { useAppStore } from '@/store/modules/app'
const appStore = useAppStore()
</script>
<style lang="scss" scoped>
.navbar-left {
flex: 1;
height: inherit;
display: flex;
align-items: center;
}
</style>
<template>
<div class="navbar-right">
<Lang />
<ComponentSize />
<Search />
<Fullscreen />
<User />
<ThemeSettings />
</div>
</template>
<script setup lang="ts">
import Lang from '@/components/lang/index.vue'
import Search from './components/Search.vue'
import ComponentSize from './components/ComponentSize.vue'
import Fullscreen from './components/Fullscreen.vue'
import User from './components/User.vue'
import ThemeSettings from './components/ThemeSettings.vue'
</script>
<style lang="scss" scoped>
.navbar-right {
display: flex;
align-items: center;
justify-content: flex-end;
&-link {
height: 100%;
display: flex;
align-items: center;
white-space: nowrap;
&-photo {
width: 25px;
height: 25px;
border-radius: 100%;
}
}
}
</style>
<template>
<el-breadcrumb separator="/" :separator-icon="ArrowRight" class="navbar-breadcrumb">
<el-breadcrumb-item key="home">
<span>{{ $t('router.home') }}</span>
</el-breadcrumb-item>
<el-breadcrumb-item v-for="(item, index) in breadcrumb" :key="index">
<span>{{ item }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { ArrowRight } from '@element-plus/icons-vue'
import { computed } from 'vue'
const route = useRoute()
const breadcrumb = computed(() => route.meta.breadcrumb)
</script>
<style lang="scss" scoped>
.navbar-breadcrumb {
::v-deep(.el-breadcrumb__inner) {
color: var(--theme-header-text-color) !important;
}
padding-left: 10px;
}
</style>
<template>
<el-dropdown trigger="click" @command="componentSizeChange">
<svg-icon icon="icon-font-size"></svg-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="large" :disabled="componentSize === 'large'">{{ $t('app.large') }}</el-dropdown-item>
<el-dropdown-item command="default" :disabled="componentSize === 'default'">{{ $t('app.default') }}</el-dropdown-item>
<el-dropdown-item command="small" :disabled="componentSize === 'small'">{{ $t('app.small') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { useAppStore } from '@/store/modules/app'
import { computed } from 'vue'
const appStore = useAppStore()
const componentSize = computed(() => appStore.componentSize)
const componentSizeChange = (size: string) => {
appStore.setComponentSize(size)
}
</script>
<template>
<svg-icon :icon="isFullscreen ? 'icon-compress' : 'icon-expend'" @click="toggle" />
</template>
<script setup lang="ts">
import { useFullscreen } from '@vueuse/core'
const { isFullscreen, toggle } = useFullscreen()
</script>
<template>
<div @click="handleClick">
<svg-icon :icon="icon"></svg-icon>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useAppStore } from '@/store/modules/app'
const appStore = useAppStore()
const screenWidth = ref(0)
const handleClick = () => {
appStore.setSidebarOpened()
}
onMounted(() => {
window.addEventListener('resize', () => {
screenWidth.value = document.body.clientWidth
})
})
watch(screenWidth, (value, oldValue) => {
if (oldValue > value && value < 1000) {
appStore.setSidebarStatus(false)
} else if (oldValue < value && value > 1000) {
appStore.setSidebarStatus(true)
}
})
const icon = computed(() => (appStore.sidebarOpened ? 'icon-outdent' : 'icon-indent'))
</script>
<template>
<svg-icon icon="icon-reload" @click="refresh"></svg-icon>
</template>
<script setup lang="ts">
import { useTabsStore } from '@/store/modules/tabs'
import { useRouter, useRoute } from 'vue-router'
import { nextTick } from 'vue'
const tabsStore = useTabsStore()
const router = useRouter()
const route = useRoute()
const refresh = () => {
tabsStore.delCachedView(route).then(() => {
nextTick(() => {
router.replace({ path: '/redirect' + route.path }).catch(err => {
console.warn(err)
})
})
})
}
</script>
<template>
<div class="navbar-search">
<svg-icon icon="icon-search" @click="openSearch"></svg-icon>
<el-dialog v-model="visible" :width="280" :destroy-on-close="true" :modal="false" fullscreen :show-close="false">
<el-autocomplete
ref="menuAutocompleteRef"
v-model="menuValue"
size="large"
:prefix-icon="Search"
:fetch-suggestions="menuSearch"
placeholder="菜单搜索"
@select="handleSelect"
@blur="handleBlur"
>
<template #default="{ item }">
<svg-icon :icon="item.meta.icon" />
{{ item.meta.title }}
</template>
</el-autocomplete>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue'
import { nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useRouterStore } from '@/store/modules/router'
const routerStore = useRouterStore()
const router = useRouter()
const menuAutocompleteRef = ref()
const visible = ref(false)
const menuValue = ref('')
const openSearch = () => {
visible.value = true
nextTick(() => {
setTimeout(() => {
menuAutocompleteRef.value.focus()
})
})
}
interface Restaurant {
path: string
meta: {
title: string
}
}
// 获取菜单列表
const menuList = routerStore.searchMenu
// 菜单搜索
const menuSearch = (queryString: string, cb: any) => {
const results = queryString ? menuList.filter(createFilter(queryString)) : menuList
cb(results)
}
const createFilter: any = (queryString: string) => {
return (restaurant: Restaurant) => {
return (
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
)
}
}
const handleSelect = (item: Restaurant) => {
router.push(item.path)
visible.value = false
}
const handleBlur = () => {
visible.value = false
}
</script>
<style lang="scss" scoped>
.navbar-search {
:deep(.el-dialog) {
box-shadow: unset !important;
border-radius: 0 !important;
background: rgba(0, 0, 0, 0.5);
}
:deep(.el-autocomplete) {
width: 560px;
position: absolute;
top: 100px;
left: 50%;
transform: translateX(-50%);
}
}
</style>
<template>
<div>
<svg-icon icon="icon-ellipsis-v" @click="themeSettingsHandle"></svg-icon>
</div>
</template>
<script setup lang="ts">
import emits from '@/utils/emits'
const themeSettingsHandle = () => {
emits.emit('openThemeSettings')
}
</script>
<template>
<el-dropdown class="avatar-container" trigger="hover">
<div class="avatar-wrapper">
<el-avatar shape="circle" :size="30" :src="userStore.user.avatar"></el-avatar>
<span>{{ userStore.user.username }}</span>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu class="user-dropdown">
<router-link to="/profile/password">
<el-dropdown-item> {{ $t('router.profilePassword') }} </el-dropdown-item>
</router-link>
<el-dropdown-item divided @click="logout"> {{ $t('app.signOut') }} </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { useUserStore } from '@/store/modules/user'
import { useRouter } from 'vue-router'
import { ArrowDown } from '@element-plus/icons-vue'
const userStore = useUserStore()
const router = useRouter()
const logout = () => {
userStore.logoutAction().then(() => {
// router.push({ path: '/login' })
// 刷新页面
location.reload()
})
}
</script>
<style lang="scss" scoped>
.avatar-container {
display: flex;
align-items: center;
justify-content: flex-end;
height: var(--theme-header-height);
.avatar-wrapper {
display: flex;
align-items: center;
white-space: nowrap;
cursor: pointer;
padding: 0 8px;
color: var(--theme-header-text-color);
span {
margin-left: 6px;
}
}
//&:hover {
// background: var(--theme-header-hover-color);
//}
}
</style>
<template>
<el-card v-loading="loading">
<iframe :src="url" class="iframe" @load="load"></iframe>
</el-card>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import { RouteLocationNormalizedLoaded, useRoute } from 'vue-router'
import { replaceLinkParam } from '@/utils/tool'
const route = useRoute()
const url = ref('')
const loading = ref(false)
watch(
() => route,
value => {
if (value.path === '/iframe') {
initUrl(value)
}
},
{ deep: true }
)
onMounted(() => {
initUrl(route)
})
const initUrl = (route: RouteLocationNormalizedLoaded): void => {
loading.value = true
const { meta, query } = route
if (query.url) {
url.value = query.url as string
} else {
url.value = replaceLinkParam(meta.url as string)
}
}
const load = () => {
loading.value = false
}
</script>
<style lang="scss" scoped>
.iframe {
min-height: calc(100vh - 70px - var(--theme-header-height));
width: 100%;
border: 0;
}
</style>
<script lang="ts">
import { defineComponent, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
export default defineComponent({
created() {
const { params, query } = useRoute()
const { path } = params
const router = useRouter()
router.replace({ path: '/' + path, query }).catch(err => {
console.warn(err)
})
},
render() {
return h('div')
}
})
</script>
<template>
<div class="settings-select">
<span> {{ title }}</span>
<el-select :model-value="modelValue" size="default" style="width: 100px" :disabled="disabled" @change="handleChange($event)">
<el-option v-for="option in options" :key="option.value" :label="option.label" :value="option.value"></el-option>
</el-select>
</div>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
defineProps({
modelValue: {
type: String,
required: true
},
title: {
type: String,
required: true
},
options: {
type: Array as PropType<any[]>,
required: true,
default: () => []
},
disabled: {
type: Boolean
}
})
const emit = defineEmits(['update:modelValue', 'change'])
const handleChange = (val: any) => {
emit('update:modelValue', val)
emit('change')
}
</script>
<style lang="scss" scoped>
.settings-select {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--el-text-color-primary);
}
</style>
<template>
<div class="settings-switch">
<span> {{ title }}</span>
<el-switch :model-value="modelValue" :disabled="disabled" @change="handleChange($event)"></el-switch>
</div>
</template>
<script setup lang="ts">
defineProps({
modelValue: {
type: Boolean,
required: true
},
title: {
type: String,
required: true
},
disabled: {
type: Boolean
}
})
const emit = defineEmits(['update:modelValue', 'change'])
const handleChange = (val: boolean) => {
emit('update:modelValue', val)
emit('change')
}
</script>
<style lang="scss" scoped>
.settings-switch {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 15px;
color: var(--el-text-color-primary);
}
</style>
This diff is collapsed.
<template>
<div class="tabs-container">
<div class="tabs-item">
<el-tabs v-model="activeTabName" :class="tabsStyleClass" @tab-click="tabClick" @tab-remove="tabRemove">
<el-tab-pane v-for="tab in tabsStore.visitedViews" :key="tab" :label="tab.title" :name="tab.path" :closable="!isAffix(tab)"></el-tab-pane>
</el-tabs>
</div>
<el-dropdown class="tabs-action" trigger="click" placement="bottom-end" @command="onClose">
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :icon="Close" command="close">{{ $t('app.close') }}</el-dropdown-item>
<el-dropdown-item :icon="CircleClose" command="closeOthers">{{ $t('app.closeOthers') }}</el-dropdown-item>
<el-dropdown-item :icon="CircleCloseFilled" command="closeAll">{{ $t('app.closeAll') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
<el-icon><arrow-down /></el-icon>
</el-dropdown>
</div>
</template>
<script setup lang="ts">
import { watch, onMounted, ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { closeAllTabs, closeOthersTabs, closeTab } from '@/utils/tabs'
import { ArrowDown, Close, CircleClose, CircleCloseFilled } from '@element-plus/icons-vue'
import { useAppStore } from '@/store/modules/app'
import { useRouterStore } from '@/store/modules/router'
import { useTabsStore } from '@/store/modules/tabs'
const appStore = useAppStore()
const routerStore = useRouterStore()
const tabsStore = useTabsStore()
const route = useRoute()
const router = useRouter()
const activeTabName = ref(route.path)
const tabsStyleClass = computed(() => 'tabs-item-' + appStore.theme.tabsStyle)
// 是否固定
const isAffix = (tab: any) => {
return tab.meta && tab.meta.affix
}
watch(route, () => {
// 当前路由,添加到tabs里
if (route.name) {
addTab()
}
})
onMounted(() => {
// 初始化
initTabs()
addTab()
})
// 初始化固定tab
const initTabs = () => {
const affixTabs = getAffixTabs(routerStore.routes)
for (const tab of affixTabs) {
// 需要有tab名称
if (tab.name) {
tabsStore.addView(tab)
}
}
}
// 获取需要固定的tabs
const getAffixTabs = (routes: any) => {
let tabs: any[] = []
routes.forEach((route: any) => {
if (route.meta && route.meta.affix) {
tabs.push({
fullPath: route.path,
path: route.path,
name: route.name,
meta: { ...route.meta }
})
}
if (route.children) {
const tempTabs = getAffixTabs(route.children)
if (tempTabs.length >= 1) {
tabs = [...tabs, ...tempTabs]
}
}
})
return tabs
}
// 添加tab
const addTab = () => {
tabsStore.addView(route)
tabsStore.addCachedView(route)
activeTabName.value = route.path
}
// tab被选中
const tabClick = (tab: any) => {
tab.props.name && router.push(tab.props.name)
}
// 点击关闭tab
const tabRemove = (path: string) => {
const tab = tabsStore.visitedViews.filter((tab: any) => tab.path === path)
closeTab(router, tab[0])
}
// dropdown 关闭事件
const onClose = (type: string) => {
switch (type) {
case 'close':
closeTab(router, route)
break
case 'closeOthers':
closeOthersTabs(router, route)
break
case 'closeAll':
closeAllTabs(router, route)
break
}
}
</script>
<style lang="scss" scoped>
.tabs-container {
display: flex;
position: relative;
z-index: 6;
height: 40px;
background-color: #fff;
.tabs-item {
transition: left 0.3s;
flex-grow: 1;
overflow: hidden;
::v-deep(.el-tabs__nav-prev) {
padding: 0 10px;
border-right: var(--el-border-color-extra-light) 1px solid;
}
::v-deep(.el-tabs__nav-next) {
padding: 0 10px;
border-left: var(--el-border-color-extra-light) 1px solid;
}
::v-deep(.is-scrollable) {
padding: 0 32px;
}
::v-deep(.el-tabs__active-bar) {
height: 0;
}
::v-deep(.el-tabs__item) {
.is-icon-close {
transition: none !important;
&:hover {
color: var(--el-color-primary-light-9);
background-color: var(--el-color-primary);
border-radius: 50%;
}
}
}
}
}
.tabs-item-style-1 {
::v-deep(.el-tabs__item) {
padding: 0 15px !important;
border-right: var(--el-border-color-extra-light) 1px solid;
user-select: none;
color: #8c8c8c;
&:hover {
color: #444;
background: rgba(0, 0, 0, 0.02);
}
&.is-active {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
border-bottom: var(--el-border-color-light) 2px solid;
&:before {
background-color: var(--el-color-primary);
}
}
&:before {
content: '';
width: 9px;
height: 9px;
margin-right: 8px;
display: inline-block;
background-color: #ddd;
border-radius: 50%;
}
}
}
.tabs-item-style-2 {
::v-deep(.el-tabs__item) {
padding: 0 15px !important;
border-right: none;
user-select: none;
color: #8c8c8c;
//display: inline-block;
&:hover {
color: #444;
background: rgba(0, 0, 0, 0.02);
border-bottom: var(--el-color-primary) 2px solid;
}
&.is-active {
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
border-bottom: var(--el-color-primary) 2px solid;
&:before {
background-color: var(--el-color-primary);
}
}
}
}
.tabs-action {
height: 40px;
line-height: 40px;
box-sizing: border-box;
padding: 0 12px;
align-items: center;
cursor: pointer;
color: #666;
border-left: var(--el-border-color-extra-light) 1px solid;
border-bottom: var(--el-border-color-light) 2px solid;
}
</style>
<template>
<el-container class="layout-container layout-columns">
<el-aside class="layout-sidebar aside-expend" :class="sidebarClass">
<div class="sidebar-logo">
<el-avatar src="./favicon.ico"></el-avatar>
</div>
<el-scrollbar>
<div class="columns-menu">
<router-link to="/home">
<div class="columns-menu-item" :class="{ active: menuPath === '/home' }">
<svg-icon icon="icon-home"></svg-icon>
<span class="title">首页</span>
</div>
</router-link>
<div
v-for="menu in routerStore.menuRoutes"
:key="menu.path"
class="columns-menu-item"
:class="{ active: menuPath === menu.path }"
@click="handleMenu(menu)"
>
<svg-icon :icon="menu.meta?.icon"></svg-icon>
<span class="title">{{ menu.meta?.title }}</span>
</div>
</div>
</el-scrollbar>
</el-aside>
<el-container>
<el-header class="layout-header" :style="layoutHeaderHeight">
<div class="navbar-container" :class="headerClass">
<NavbarLeft />
<NavbarRight />
</div>
</el-header>
<div class="layout-main">
<div v-if="subMenus.length > 0 && appStore.sidebarOpened" class="columns-sub-menu">
<el-menu
:default-active="defaultActive"
:collapse="!appStore.sidebarOpened"
:unique-opened="appStore.theme.uniqueOpened"
background-color="transparent"
:collapse-transition="false"
mode="vertical"
>
<menu-item v-for="menu in subMenus" :key="menu.path" :menu="menu"></menu-item>
</el-menu>
</div>
<div class="columns-sub-main">
<Tabs v-if="theme.isTabsView" />
<Main />
</div>
</div>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import { useRouterStore } from '@/store/modules/router'
import { useAppStore } from '@/store/modules/app'
import NavbarLeft from '@/layout/components/Navbar/NavbarLeft.vue'
import NavbarRight from '@/layout/components/Navbar/NavbarRight.vue'
import Main from '@/layout/components/Main/index.vue'
import Tabs from '@/layout/components/Tabs/index.vue'
import MenuItem from '@/layout/components/Menu/MenuItem.vue'
import { computed, onMounted, ref, watch } from 'vue'
import { RouteRecordRaw, useRoute, useRouter } from 'vue-router'
const routerStore = useRouterStore()
const appStore = useAppStore()
const route = useRoute()
const router = useRouter()
const defaultActive = computed(() => {
const { path } = route
return path
})
const subMenus = ref<any[]>([])
watch(route, () => {
subMenus.value = []
initSubMenu()
})
onMounted(() => {
initSubMenu()
})
const menuPath = ref<string>('')
const initSubMenu = () => {
menuPath.value = defaultActive.value
for (const menu of routerStore.menuRoutes) {
// 是否包含当前路由
const exist = findRoute(menu.children as RouteRecordRaw[])
if (exist) {
subMenus.value = menu.children as RouteRecordRaw[]
menuPath.value = menu.path
break
}
}
}
const findRoute = (menus: RouteRecordRaw[]): boolean => {
for (const menu of menus) {
// 有子菜单的情况
if (menu.children && menu.children.length > 0) {
if (findRoute(menu.children)) {
return true
}
} else if (menu.path === defaultActive.value) {
return true
}
}
return false
}
const handleMenu = (menu: any) => {
if (menu.children && menu.children.length > 0) {
const leafRoute = findLeafRoute(menu.children)
router.push(leafRoute.path)
} else {
router.push(menu.path)
}
}
const findLeafRoute = (menus: RouteRecordRaw[]): any => {
for (const menu of menus) {
// 有子菜单的情况
if (menu.children && menu.children.length > 0) {
return findLeafRoute(menu.children)
} else {
return menu
}
}
return null
}
const headerClass = computed(() => (appStore.theme.headerStyle === 'theme' ? 'header-theme' : ''))
const sidebarClass = computed(() => {
return appStore.theme.sidebarStyle === 'dark' ? 'sidebar-dark' : ''
})
const theme = computed(() => appStore.theme)
const layoutHeaderHeight = computed(() => {
if (!theme.value.isTabsView) {
return 'height:var(--theme-header-height) !important'
} else {
return ''
}
})
</script>
<style lang="scss" scoped>
.sidebar-logo {
height: var(--theme-header-height);
line-height: var(--theme-header-height);
border-bottom: var(--theme-border-color-light) 1px solid;
text-align: center;
.el-avatar {
width: 30px;
height: 30px;
vertical-align: middle;
}
}
.columns-menu {
flex: 1;
.columns-menu-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 70px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background-color: var(--el-color-primary);
}
::v-deep(.svg-icon) {
align-items: center;
cursor: pointer;
svg {
font-size: 21px;
color: var(--theme-menu-text-color) !important;
}
}
.title {
margin-top: 6px;
font-size: 12px;
color: var(--theme-menu-text-color);
}
}
.active {
background-color: var(--el-color-primary);
}
}
.layout-sidebar {
&.aside-expend {
width: 75px !important;
}
::v-deep(.el-menu-item) {
padding-left: 6px !important;
padding-right: 10px !important;
}
}
.layout-header {
height: var(--theme-header-height) !important;
border-bottom: 51px solid #8c8c8c !important;
}
.layout-main {
display: flex;
width: 100%;
height: 100%;
.el-menu {
border-right: none !important;
}
.el-menu-item {
height: 40px !important;
line-height: 40px !important;
font-size: 14px !important;
color: var(--theme-menu-text-color) !important;
&:hover {
color: var(--el-color-primary) !important;
}
}
.el-menu-item.is-active {
border-right: none;
right: 0;
color: var(--theme-menu-hover-color) !important;
}
.columns-sub-main {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
width: 100%;
overflow: hidden;
}
}
.column-menu {
display: block;
}
.columns-sub-menu {
width: 120px;
height: 100%;
display: block;
float: left;
border-right: var(--theme-border-color-light) 1px solid;
background: #fff;
}
.navbar-container {
height: var(--theme-header-height);
display: flex;
align-items: center;
background: var(--theme-header-bg-color);
border-bottom: 1px solid var(--theme-border-color-light);
color: var(--theme-header-text-color);
::v-deep(.svg-icon) {
align-items: center;
cursor: pointer;
height: var(--theme-header-height);
line-height: var(--theme-header-height);
padding: 0 12px;
svg {
color: var(--theme-header-text-color) !important;
font-size: 16px;
}
&:hover {
background: var(--theme-header-hover-color);
}
}
}
</style>
<template>
<el-container class="layout-container layout-transverse">
<el-header class="navbar-container" :class="headerClass">
<Logo v-if="theme.isLogo" />
<el-menu :default-active="defaultActive" background-color="transparent" :collapse-transition="false" mode="horizontal">
<menu-item v-for="menu in routerStore.menuRoutes" :key="menu.path" :menu="menu"></menu-item>
</el-menu>
<NavbarRight />
</el-header>
<Tabs v-if="theme.isTabsView" />
<Main />
</el-container>
</template>
<script setup lang="ts">
import NavbarRight from '@/layout/components/Navbar/NavbarRight.vue'
import Main from '@/layout/components/Main/index.vue'
import Tabs from '@/layout/components/Tabs/index.vue'
import Logo from '@/layout/components/Logo/index.vue'
import MenuItem from '@/layout/components/Menu/MenuItem.vue'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useRouterStore } from '@/store/modules/router'
import { useAppStore } from '@/store/modules/app'
const appStore = useAppStore()
const routerStore = useRouterStore()
const theme = computed(() => appStore.theme)
const route = useRoute()
const defaultActive = computed(() => {
const { path } = route
return path
})
const headerClass = computed(() => (appStore.theme.headerStyle === 'theme' ? 'header-theme' : ''))
</script>
<style lang="scss" scoped>
.layout-container {
width: 100%;
height: 100%;
.el-header {
padding-right: 0 !important;
}
}
.navbar-container {
height: var(--theme-header-height);
display: flex;
align-items: center;
background: var(--theme-header-bg-color);
border-bottom: 1px solid var(--theme-border-color-light);
color: var(--theme-header-text-color);
::v-deep(.el-sub-menu__title) {
&:hover {
background: var(--theme-header-hover-color) !important;
}
}
::v-deep(.svg-icon) {
align-items: center;
cursor: pointer;
height: var(--theme-header-height);
line-height: var(--theme-header-height);
padding: 0 12px;
svg {
font-size: 16px;
}
&:hover {
background: var(--theme-header-hover-color);
}
}
.el-menu {
width: 100%;
display: flex;
align-items: center;
flex-direction: row;
margin-left: 20px;
::v-deep(.svg-icon) {
&:hover {
background: none !important;
}
padding: 0;
}
}
}
</style>
<template>
<el-container class="layout-container layout-vertical">
<el-aside class="layout-sidebar" :class="sidebarClass">
<Logo v-if="appStore.theme.isLogo" />
<el-scrollbar>
<el-menu
:default-active="defaultActive"
:collapse="!appStore.sidebarOpened"
:unique-opened="appStore.theme.uniqueOpened"
background-color="transparent"
:collapse-transition="false"
mode="vertical"
>
<menu-item v-for="menu in routerStore.menuRoutes" :key="menu.path" :menu="menu"></menu-item>
</el-menu>
</el-scrollbar>
</el-aside>
<el-container>
<el-header class="layout-header" :style="layoutHeaderHeight">
<div class="navbar-container" :class="headerClass">
<NavbarLeft />
<NavbarRight />
</div>
<Tabs v-if="theme.isTabsView" />
</el-header>
<Main />
</el-container>
</el-container>
</template>
<script setup lang="ts">
import NavbarLeft from '@/layout/components/Navbar/NavbarLeft.vue'
import NavbarRight from '@/layout/components/Navbar/NavbarRight.vue'
import Main from '@/layout/components/Main/index.vue'
import Tabs from '@/layout/components/Tabs/index.vue'
import Logo from '@/layout/components/Logo/index.vue'
import MenuItem from '@/layout/components/Menu/MenuItem.vue'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useAppStore } from '@/store/modules/app'
import { useRouterStore } from '@/store/modules/router'
const appStore = useAppStore()
const routerStore = useRouterStore()
const route = useRoute()
const defaultActive = computed(() => {
const { path } = route
return path
})
const headerClass = computed(() => (appStore.theme.headerStyle === 'theme' ? 'header-theme' : ''))
const sidebarClass = computed(() => {
const sidebarOpened = appStore.sidebarOpened ? 'aside-expend' : 'aside-compress'
const isDark = appStore.theme.sidebarStyle === 'dark' ? 'sidebar-dark' : ''
return sidebarOpened + ' ' + isDark
})
const theme = computed(() => appStore.theme)
const layoutHeaderHeight = computed(() => {
if (!theme.value.isTabsView) {
return 'height:var(--theme-header-height) !important'
} else {
return ''
}
})
</script>
<style lang="scss" scoped>
.navbar-container {
height: var(--theme-header-height);
display: flex;
align-items: center;
background: var(--theme-header-bg-color);
border-bottom: 1px solid var(--theme-border-color-light);
color: var(--theme-header-text-color);
::v-deep(.svg-icon) {
align-items: center;
cursor: pointer;
height: var(--theme-header-height);
line-height: var(--theme-header-height);
padding: 0 12px;
svg {
color: var(--theme-header-text-color) !important;
font-size: 16px;
}
&:hover {
background: var(--theme-header-hover-color);
}
}
}
</style>
<template>
<component :is="LayoutComponents[layout]"></component>
<Settings></Settings>
</template>
<script setup lang="ts">
import { useAppStore } from '@/store/modules/app'
import Settings from '@/layout/components/Settings/index.vue'
import Vertical from '@/layout/components/Theme/Vertical.vue'
import Columns from '@/layout/components/Theme/Columns.vue'
import Transverse from '@/layout/components/Theme/Transverse.vue'
import { computed } from 'vue'
const appStore = useAppStore()
const LayoutComponents: { [key: string]: any } = {
vertical: Vertical,
columns: Columns,
transverse: Transverse
}
const layout = computed(() => appStore.theme.layout)
</script>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment