@ -1,5 +1,5 @@
|
||||
VITE_BASE_URL=/api
|
||||
VITE_BASE_URL=http://192.168.10.27:8090
|
||||
VITE_SOCKET_URL=wss://k8s-horse-gateway.mashibing.cn/ws
|
||||
#VITE_SOCKET_URL=ws://192.168.10.93:8090/ws
|
||||
VITE_REQUEST_TIMEOUT=5000
|
||||
VITE_BROWSER_URL = https://k8s-shop-pc.mashibing.cn
|
||||
VITE_BROWSER_URL = https://k8s-shop-pc.mashibing.cn
|
||||
|
||||
VITE_REQUEST_TIMEOUT=5000
|
@ -0,0 +1,4 @@
|
||||
VITE_BASE_URL=https://you-gateway.mashibing.com
|
||||
VITE_SOCKET_URL=wss://you-gateway.mashibing.com/ws
|
||||
VITE_REQUEST_TIMEOUT=20000
|
||||
VITE_BROWSER_URL = https://you.mashibing.com
|
@ -1,4 +1,4 @@
|
||||
VITE_BASE_URL=https://k8s-horse-gateway.mashibing.cn/
|
||||
VITE_BASE_URL=https://k8s-horse-gateway.mashibing.cn
|
||||
VITE_SOCKET_URL=wss://k8s-horse-gateway.mashibing.cn/ws
|
||||
VITE_REQUEST_TIMEOUT=20000
|
||||
VITE_BROWSER_URL = https://k8s-shop-pc.mashibing.cn
|
||||
|
@ -0,0 +1,2 @@
|
||||
VITE_BASE_URL=/api
|
||||
VITE_REQUEST_TIMEOUT=20000
|
@ -0,0 +1,2 @@
|
||||
VITE_BASE_URL=https://gateway.mashibing.cn
|
||||
VITE_REQUEST_TIMEOUT=20000
|
@ -0,0 +1,2 @@
|
||||
VITE_BASE_URL=https://gateway.mashibing.com
|
||||
VITE_REQUEST_TIMEOUT=20000
|
@ -0,0 +1,2 @@
|
||||
VITE_BASE_URL=https://gateway-test.mashibing.cn
|
||||
VITE_REQUEST_TIMEOUT=20000
|
@ -0,0 +1,34 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
es6: true,
|
||||
node: true,
|
||||
browser: true,
|
||||
},
|
||||
extends: ['eslint:recommended'],
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
impliedStrict: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error',
|
||||
'no-alert': 'warn',
|
||||
'no-debugger': 'warn',
|
||||
'no-else-return': 'error',
|
||||
indent: [0, 4],
|
||||
eqeqeq: [2, 'always'],
|
||||
semi: [2, 'always'],
|
||||
quotes: [2, 'single'],
|
||||
},
|
||||
globals: {
|
||||
arguments: true,
|
||||
defineProps: true,
|
||||
defineEmits: true,
|
||||
defineExpose: true,
|
||||
},
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
tabWidth: 4,
|
||||
useTabs: false,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
quoteProps: 'as-needed',
|
||||
jsxSingleQuote: false,
|
||||
trailingComma: 'es5',
|
||||
bracketSpacing: true,
|
||||
bracketSameLine: false,
|
||||
arrowParens: 'always',
|
||||
htmlWhitespaceSensitivity: 'ignore',
|
||||
vueIndentScriptAndStyle: true,
|
||||
endOfLine: 'auto',
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["johnsoncodehk.volar", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
{
|
||||
// Place your shop-admin 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||
// Placeholders with the same ids are connected.
|
||||
// Example:
|
||||
// "Print to console": {
|
||||
// "scope": "javascript,typescript",
|
||||
// "prefix": "log",
|
||||
// "body": [
|
||||
// "console.log('$1');",
|
||||
// "$2"
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
"import element plus component": {
|
||||
"scope": "javascript,typescript",
|
||||
"prefix": "import ele",
|
||||
"body": [
|
||||
"import { ${1:El} } from 'element-plus/es/components/$2/index';",
|
||||
"import 'element-plus/es/components/$2/style/css';"
|
||||
],
|
||||
"description": "按需引用ElementPlus组件及其样式"
|
||||
},
|
||||
"vbase extra component": {
|
||||
"scope": "vue",
|
||||
"prefix": "vbase extra",
|
||||
"body": [
|
||||
"<template>\n<component :is=\"render\" />\n</template>",
|
||||
"<script setup lang=\"jsx\">",
|
||||
"import { ${1:El} } from 'element-plus/es/components/$2/index';",
|
||||
"import 'element-plus/es/components/$2/style/css';",
|
||||
"const props = defineProps({});",
|
||||
"const attrs = useAttrs();",
|
||||
"const slots = useSlots();",
|
||||
"const render = () => <$1 {...props} {...attrs} v-slots={slots} />;",
|
||||
"</script>",
|
||||
"<style lang=\"less\" scoped></style>"
|
||||
],
|
||||
"description": "快速二次封装ElementPlus组件"
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link href="/src/styles/loading.less" rel="stylesheet" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="loading-container">
|
||||
<div class="dot-wrapper">
|
||||
<span class="dot">
|
||||
<i></i>
|
||||
<i></i>
|
||||
<i></i>
|
||||
<i></i>
|
||||
</span>
|
||||
</div>
|
||||
<h1>加载中</h1>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "Node",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "msb-shop-admin",
|
||||
"author": {
|
||||
"name": "向文可",
|
||||
"email": "1041367524@qq.com"
|
||||
},
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:test": "vite build --mode test",
|
||||
"build:preview": "vite build --mode preview",
|
||||
"build:prod": "vite build --mode prod",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons": "^0.0.11",
|
||||
"axios": "^0.26.1",
|
||||
"dayjs": "^1.11.0",
|
||||
"element-plus": "^2.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"qs": "^6.10.3",
|
||||
"sortablejs": "^1.14.0",
|
||||
"vue": "^3.2.25",
|
||||
"vue-router": "^4.0.14",
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@originjs/vite-plugin-global-style": "^1.0.2",
|
||||
"@types/node": "^17.0.21",
|
||||
"@vitejs/plugin-legacy": "^1.7.1",
|
||||
"@vitejs/plugin-vue": "^2.2.0",
|
||||
"@vitejs/plugin-vue-jsx": "^1.3.8",
|
||||
"consola": "^2.15.3",
|
||||
"less": "^4.1.2",
|
||||
"unplugin-auto-import": "^0.6.4",
|
||||
"unplugin-vue-components": "^0.18.0",
|
||||
"vite": "^2.8.0",
|
||||
"vite-plugin-remove-console": "^0.0.6",
|
||||
"vite-plugin-style-import": "^2.0.0",
|
||||
"vite-plugin-svg-icons": "^2.0.1"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<el-config-provider
|
||||
:locale="config.locale"
|
||||
:size="config.size"
|
||||
:z-index="config.zIndex"
|
||||
:button="config.button"
|
||||
:message="config.message"
|
||||
>
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import zh from 'element-plus/lib/locale/lang/zh-cn';
|
||||
const route = useRoute();
|
||||
const config = reactive({
|
||||
locale: zh,
|
||||
size: 'default',
|
||||
zIndex: 300,
|
||||
button: {
|
||||
autoInsertSpace: true,
|
||||
},
|
||||
message: {
|
||||
max: 5,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import url('@/styles/base.less');
|
||||
</style>
|
@ -0,0 +1,40 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 获取验证码
|
||||
export function sendSmsCode(params) {
|
||||
return request({
|
||||
url: '/uaa/sms/sendSms',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
// 登录
|
||||
export async function login(data) {
|
||||
return request({
|
||||
url: '/uaa/sso/appManageLogin',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
// 获取用户信息
|
||||
export function getUserInfo() {
|
||||
return request({
|
||||
url: '/uc/user/v1/info/token',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
// 获取权限列表
|
||||
export function getPermission(params) {
|
||||
return request({
|
||||
url: '/u-admin/uc/ucPermission/listUserMenu',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
// 退出登录
|
||||
export function logout() {
|
||||
return request({
|
||||
url: '/uaa/sso/v1/logout',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
const mock = (data) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(data);
|
||||
}, Math.random() * 1500 + 500);
|
||||
});
|
||||
export const findUserList = (data) => {
|
||||
return mock({
|
||||
content: [
|
||||
{
|
||||
id: 1,
|
||||
username: 'user001',
|
||||
nickname: '张三',
|
||||
sex: 1,
|
||||
avatar: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2Ffd%2Ff1%2Fda%2Ffdf1dacb8ff0b8f13ed29bcbee42f328.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1650201540&t=ba213738d8f11e79302fab71602856f2',
|
||||
loginTime: Date.now(),
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'user003',
|
||||
nickname: '李四',
|
||||
sex: 0,
|
||||
avatar: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2Ffd%2Ff1%2Fda%2Ffdf1dacb8ff0b8f13ed29bcbee42f328.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1650201540&t=ba213738d8f11e79302fab71602856f2',
|
||||
loginTime: Date.now(),
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'user003',
|
||||
nickname: '王五',
|
||||
sex: 1,
|
||||
avatar: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2Ffd%2Ff1%2Fda%2Ffdf1dacb8ff0b8f13ed29bcbee42f328.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1650201540&t=ba213738d8f11e79302fab71602856f2',
|
||||
loginTime: Date.now(),
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
totalElements: 3,
|
||||
});
|
||||
};
|
After Width: | Height: | Size: 323 KiB |
After Width: | Height: | Size: 443 B |
After Width: | Height: | Size: 9.0 KiB |
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<component :is="render" />
|
||||
</template>
|
||||
<script setup lang="jsx">
|
||||
import { ElButton } from 'element-plus/es/components/button/index';
|
||||
import 'element-plus/es/components/button/style/css';
|
||||
const props = defineProps({});
|
||||
const attrs = useAttrs();
|
||||
const slots = useSlots();
|
||||
const render = () => <ElButton {...props} {...attrs} v-slots={slots} />;
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.el-button {
|
||||
:deep(.el-icon) {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<component :is="render" />
|
||||
</template>
|
||||
<script setup lang="jsx">
|
||||
import { ElCascader } from 'element-plus/es/components/cascader/index';
|
||||
import 'element-plus/es/components/cascader/style/css';
|
||||
const props = defineProps({
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
const attrs = useAttrs();
|
||||
const slots = useSlots();
|
||||
const render = () => <ElCascader {...props} {...attrs} v-slots={slots}></ElCascader>;
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<component :is="render" />
|
||||
</template>
|
||||
<script setup lang="jsx">
|
||||
import { ElCheckboxGroup } from 'element-plus/es/components/checkbox/index';
|
||||
import 'element-plus/es/components/checkbox/style/css';
|
||||
const props = defineProps({
|
||||
opts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
disabled: 'disabled',
|
||||
};
|
||||
},
|
||||
},
|
||||
button: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const slots = useSlots();
|
||||
const attrs = useAttrs();
|
||||
let config = {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
disabled: 'disabled',
|
||||
...props.config,
|
||||
};
|
||||
function handleItemDisabled(item, index) {
|
||||
let res = false;
|
||||
if (config.disabled instanceof Function) {
|
||||
res = config.disabled(item, index);
|
||||
} else {
|
||||
res = !!item[config.disabled];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
const render = () => (
|
||||
<ElCheckboxGroup
|
||||
{...props}
|
||||
{...attrs}
|
||||
v-slots={{
|
||||
default: () =>
|
||||
props.opts.map((item, index) => (
|
||||
<ElCheckbox label={item[config.value]} disabled={handleItemDisabled(item, index)}>
|
||||
{item[config.label]}
|
||||
</ElCheckbox>
|
||||
)),
|
||||
...slots,
|
||||
}}
|
||||
></ElCheckboxGroup>
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<component :is="render" />
|
||||
</template>
|
||||
<script setup lang="jsx">
|
||||
import { ElScrollbar } from 'element-plus/es/components/scrollbar/index';
|
||||
import { ElDialog } from 'element-plus/es/components/dialog/index';
|
||||
import 'element-plus/es/components/dialog/style/css';
|
||||
import 'element-plus/es/components/scrollbar/style/css';
|
||||
const props = defineProps({
|
||||
destroyOnClose: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
closeOnClickModal: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
closeOnPressEscape: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['update:fullscreen']);
|
||||
const slots = useSlots();
|
||||
const attrs = useAttrs();
|
||||
const fullscreen = ref(attrs.fullscreen || false);
|
||||
const handleFullScreen = () => {
|
||||
fullscreen.value = !unref(fullscreen);
|
||||
emit('update:fullscreen', unref(fullscreen));
|
||||
};
|
||||
const dialogSlots = {
|
||||
...slots,
|
||||
default() {
|
||||
return (
|
||||
<ElScrollbar max-height={unref(fullscreen) ? '100%' : '60vh'}>
|
||||
<div>{slots.default?.()}</div>
|
||||
</ElScrollbar>
|
||||
);
|
||||
},
|
||||
title() {
|
||||
return (
|
||||
<div class="el-dialog__header-wrapper">
|
||||
{slots.title?.() || <span class="el-dialog__title">{attrs.title || '弹窗'}</span>}
|
||||
<ElButton
|
||||
aria-label="fullscreen"
|
||||
class="el-dialog__headerbtn fullscreen-btn"
|
||||
type="text"
|
||||
onClick={() => handleFullScreen()}
|
||||
>
|
||||
<ElIcon class="el-dialog__fullscreen" name={unref(fullscreen) ? 'Minus' : 'FullScreen'} />
|
||||
</ElButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
const render = () => <ElDialog {...props} {...attrs} fullscreen={unref(fullscreen)} v-slots={dialogSlots} />;
|
||||
</script>
|
||||
<style lang="less">
|
||||
.el-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
&.is-fullscreen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.el-dialog__body {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.el-dialog__header {
|
||||
padding: @layout-space-large;
|
||||
.el-dialog__header-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.fullscreen-btn {
|
||||
position: static;
|
||||
height: auto;
|
||||
color: var(--el-button-text-color, var(--el-text-color-regular));
|
||||
:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-dialog__body {
|
||||
padding: @layout-space-large;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.el-scrollbar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.el-dialog__headerbtn {
|
||||
top: calc(@layout-space-large + 5px);
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<component :is="render" />
|
||||
</template>
|
||||
<script setup lang="jsx">
|
||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus/es/components/dropdown/index';
|
||||
import 'element-plus/es/components/dropdown/style/css';
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'ArrowDown',
|
||||
},
|
||||
splitButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
opts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const attrs = useAttrs();
|
||||
const slots = useSlots();
|
||||
const handleCommand = (index) => {
|
||||
props.opts[index].onClick?.();
|
||||
};
|
||||
const dropdownSlots = {
|
||||
dropdown: () => (
|
||||
<ElDropdownMenu>
|
||||
{(props.opts || []).map((item, index) => (
|
||||
<ElDropdownItem command={index} disabled={item.disabled} divided={item.divided}>
|
||||
{item.label}
|
||||
</ElDropdownItem>
|
||||
))}
|
||||
</ElDropdownMenu>
|
||||
),
|
||||
default: () => (
|
||||
<ElButton type="text">
|
||||
{slots.default?.() || '更多'}
|
||||
<ElIcon name={props.icon} size="16" />
|
||||
</ElButton>
|
||||
),
|
||||
};
|
||||
const render = () => (
|
||||
<ElDropdown {...props} {...attrs} onCommand={(command) => handleCommand(command)} v-slots={dropdownSlots} />
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.el-dropdown {
|
||||
:deep(.x-icon) {
|
||||
top: 0;
|
||||
margin-left: @layout-space-small;
|
||||
}
|
||||
}
|
||||
.el-button + .el-dropdown {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<template v-if="name">
|
||||
<svg v-if="svg" class="x-icon" aria-hidden="true">
|
||||
<use :href="symbolId" :fill="color" />
|
||||
</svg>
|
||||
<i v-else-if="isRemix" class="x-icon" :class="'x-icon-' + name" v-bind="{ ...$props, ...$attrs }"></i>
|
||||
<ElIcon v-else class="x-icon">
|
||||
<component :is="icons[name]" />
|
||||
</ElIcon>
|
||||
</template>
|
||||
</template>
|
||||
<script setup>
|
||||
import * as icons from '@element-plus/icons';
|
||||
import { ElIcon } from 'element-plus/es/components/icon/index';
|
||||
const props = defineProps({
|
||||
// 图标名称
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 图标尺寸
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: 14,
|
||||
},
|
||||
// 是否为自定义SVG图标
|
||||
svg: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 图标颜色
|
||||
color: {
|
||||
type: String,
|
||||
default: 'inherit',
|
||||
},
|
||||
});
|
||||
// 自定义SVG唯一标识
|
||||
const symbolId = computed(() => `#icon-${props.name}`);
|
||||
// 补全图标尺寸
|
||||
const size = computed(() => (Number.isNaN(new Number(props.size).valueOf()) ? props.size : props.size + 'px'));
|
||||
// 判断是remix图标还是element-plus图标
|
||||
const isRemix = computed(() => !Object.keys(icons).includes(props.name));
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.x-icon {
|
||||
font-size: v-bind(size);
|
||||
color: v-bind(color);
|
||||
line-height: 1;
|
||||
}
|
||||
svg.x-icon {
|
||||
width: v-bind(size);
|
||||
height: v-bind(size);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<component :is="render" />
|
||||
</template>
|
||||
<script setup lang="jsx">
|
||||
import { ElImage } from 'element-plus/es/components/image/index';
|
||||
import 'element-plus/es/components/image/style/css';
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: 'none',
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: 'auto',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'auto',
|
||||
},
|
||||
fit: {
|
||||
type: String,
|
||||
default: 'contain',
|
||||
},
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 9999,
|
||||
},
|
||||
hideOnClickModal: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
previewSrcList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
previewTeleported: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
const slots = useSlots();
|
||||
const attrs = useAttrs();
|
||||
const imageSlots = {
|
||||
placeholder: () => (
|
||||
<div class="image-slot">
|
||||
<ElIcon name="Picture" size="20" />
|
||||
</div>
|
||||
),
|
||||
error: () => (
|
||||
<div class="image-slot">
|
||||
<ElIcon name="file-damage-fill" size="20" />
|
||||
</div>
|
||||
),
|
||||
...slots,
|
||||
};
|
||||
if (props.previewSrcList?.length === 0) {
|
||||
props.previewSrcList.push(props.src);
|
||||
}
|
||||
const width = computed(() => props.width);
|
||||
const height = computed(() => props.height);
|
||||
const render = () => <ElImage {...props} {...attrs} v-slots={imageSlots} />;
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.el-image {
|
||||
width: v-bind(width);
|
||||
height: v-bind(height);
|
||||
:deep(.image-slot) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f5f7fa;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<component :is="render" />
|
||||
</template>
|
||||
<script setup lang="jsx">
|
||||
import { ElInput } from 'element-plus/es/components/input/index';
|
||||
import 'element-plus/es/components/input/style/css';
|
||||
const props = defineProps({
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showWordLimit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请输入',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
showPassword: {
|
||||
type: Boolean,
|
||||
default(props) {
|
||||
return props.type === 'password';
|
||||
},
|
||||
},
|
||||
showWordLimit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
autosize: {
|
||||
type: [Boolean, Object],
|
||||
default() {
|
||||
return { minRows: 5, maxRows: 10 };
|
||||
},
|
||||
},
|
||||
resize: {
|
||||
type: String,
|
||||
default: 'none',
|
||||
},
|
||||
});
|
||||
const attrs = useAttrs();
|
||||
const slots = useSlots();
|
||||
const render = () => <ElInput {...props} {...attrs} v-slots={slots}></ElInput>;
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<component :is="render" />
|
||||
</template>
|
||||
<script setup lang="jsx">
|
||||
import { ElRadioGroup, ElRadioButton, ElRadio } from 'element-plus/es/components/radio/index';
|
||||
import 'element-plus/es/components/radio/style/css';
|
||||
import 'element-plus/es/components/radio-button/style/css';
|
||||
const props = defineProps({
|
||||
opts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
disabled: 'disabled',
|
||||
};
|
||||
},
|
||||
},
|
||||
button: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const attrs = useAttrs();
|
||||
const slots = useSlots();
|
||||
let config = {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
disabled: 'disabled',
|
||||
...props.config,
|
||||
};
|
||||
function handleItemDisabled(item, index) {
|
||||
let res = false;
|
||||
if (config.disabled instanceof Function) {
|
||||
res = config.disabled(item, index);
|
||||
} else {
|
||||
res = !!item[config.disabled];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
const Component = props.button ? ElRadioButton : ElRadio;
|
||||
const render = () => (
|
||||
<ElRadioGroup
|
||||
{...props}
|
||||
{...attrs}
|
||||
v-slots={{
|
||||
default: () =>
|
||||
props.opts.map((item, index) => (
|
||||
<Component label={item[config.value]} disabled={handleItemDisabled(item, index)}>
|
||||
{item[config.label]}
|
||||
</Component>
|
||||
)),
|
||||
...slots,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<component :is="render" />
|
||||
</template>
|
||||
<script setup lang="jsx">
|
||||
import { ElSelect, ElOption } from 'element-plus/es/components/select/index';
|
||||
import 'element-plus/es/components/select/style/css';
|
||||
const props = defineProps({
|
||||
opts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
filterable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
disabled: 'disabled',
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const slots = useSlots();
|
||||
const attrs = useAttrs();
|
||||
let config = {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
disabled: 'disabled',
|
||||
...props.config,
|
||||
};
|
||||
function handleItemDisabled(item, index) {
|
||||
let res = false;
|
||||
if (config.disabled instanceof Function) {
|
||||
res = config.disabled(item, index);
|
||||
} else {
|
||||
res = !!item[config.disabled];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
const opts = computed(() =>
|
||||
props.opts.map((item) => {
|
||||
return typeof item !== 'object' ? { label: item, value: item } : item;
|
||||
})
|
||||
);
|
||||
const render = () => (
|
||||
<ElSelect
|
||||
{...props}
|
||||
{...attrs}
|
||||
v-slots={{
|
||||
default: () =>
|
||||
unref(opts).map((item, index) => (
|
||||
<ElOption
|
||||
label={item[config.label]}
|
||||
value={item[config.value]}
|
||||
disabled={handleItemDisabled(item, index)}
|
||||
>
|
||||
{slots.option?.(item, index) || item[config.label]}
|
||||
</ElOption>
|
||||
)),
|
||||
...slots,
|
||||
}}
|
||||
></ElSelect>
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<component :is="render" />
|
||||
</template>
|
||||
<script setup lang="jsx">
|
||||
import config from '@/configs';
|
||||
import { ElMessage } from '@/plugins/element-plus';
|
||||
import { ElUpload } from 'element-plus/es/components/upload/index';
|
||||
import 'element-plus/es/components/upload/style/css';
|
||||
import { ElImage } from 'element-plus/es/components/image/index';
|
||||
import 'element-plus/es/components/image/style/css';
|
||||
const store = useStore();
|
||||
const props = defineProps({
|
||||
action: {
|
||||
type: String,
|
||||
default: config.baseURL + '/edu-oss/oss/fileUpload',
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default() {
|
||||
return { service: 'msb-edu-course' };
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
drag: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 1024 * 1024 * 20,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: '*.*',
|
||||
},
|
||||
});
|
||||
const attrs = useAttrs();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
props.headers['Authorization'] = 'Bearer ' + store.state.local.token;
|
||||
let imgList = ref([]);
|
||||
const refsUpload = ref(null);
|
||||
watch(
|
||||
() => imgList,
|
||||
() => {
|
||||
if (props.limit === 1) {
|
||||
emits('update:modelValue', unref(imgList)[0]?.response.data);
|
||||
} else {
|
||||
emits('update:modelValue', unref(imgList));
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
const handleSuccess = (res, file, list) => {
|
||||
console.info('[upload] success', list);
|
||||
imgList.value = list;
|
||||
};
|
||||
const handleRemove = (file, list) => {
|
||||
console.info('[upload] remove', list);
|
||||
imgList.value = list;
|
||||
};
|
||||
const handleExceed = (list) => {
|
||||
console.info('[upload] exceed', list);
|
||||
ElMessage.error('超出最大上传数量');
|
||||
};
|
||||
const handleBeforeUpload = (file) => {
|
||||
console.info('[upload] upload', file);
|
||||
if (file.size >= props.size) {
|
||||
ElMessage.error('超出文件大小限制');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const fmtSize = computed(() => {
|
||||
const units = ['byte', 'KB', 'MB', 'GB', 'TB'];
|
||||
let res = props.size,
|
||||
unit = 0;
|
||||
while (res >= 800) {
|
||||
res /= 1024;
|
||||
unit++;
|
||||
}
|
||||
return res + units[unit];
|
||||
});
|
||||
watch(
|
||||
() => attrs.modelValue,
|
||||
(value) => {
|
||||
if (props.limit === 1 && value) {
|
||||
imgList.value = [
|
||||
{
|
||||
name: value,
|
||||
response: {
|
||||
data: value,
|
||||
},
|
||||
},
|
||||
];
|
||||
} else {
|
||||
imgList.value = value || [];
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
const handleDeleteImage = (index) => {
|
||||
if (unref(refsUpload)) {
|
||||
unref(refsUpload).handleRemove(imgList[index]);
|
||||
} else {
|
||||
unref(imgList).splice(index, 1);
|
||||
}
|
||||
};
|
||||
const render = () => (
|
||||
<div class="upload-box">
|
||||
<div class="upload-image">
|
||||
{unref(imgList).map((item, index) => (
|
||||
<div class="img-li">
|
||||
<ElImage src={item?.response?.data} alt={item.name} />
|
||||
<div class="img-li-cover" onClick={() => handleDeleteImage(index)}>
|
||||
<ElIcon class="upload-del-icon" name="delete-bin-fill" size="20" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{props.limit != unref(imgList).length ? (
|
||||
<ElUpload
|
||||
ref={refsUpload}
|
||||
{...props}
|
||||
{...attrs}
|
||||
before-upload={handleBeforeUpload}
|
||||
on-exceed={handleExceed}
|
||||
on-remove={handleRemove}
|
||||
on-success={handleSuccess}
|
||||
show-file-list={false}
|
||||
>
|
||||
<ElIcon class="el-icon--upload" name="add-fill" size="20" />
|
||||
</ElUpload>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
<div class="el-upload__tip">支持小于 {unref(fmtSize)} 的 文件</div>
|
||||
</div>
|
||||
);
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.upload-box {
|
||||
:deep(.upload-image) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.img-li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin-right: 20px;
|
||||
overflow: hidden;
|
||||
.img-li-cover {
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
.img-li-cover {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
cursor: pointer;
|
||||
.upload-del-icon {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.el-upload) {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
.el-upload-dragger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.el-icon--upload {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,14 @@
|
||||
export default {
|
||||
/**
|
||||
* 接口请求地址前缀
|
||||
*/
|
||||
baseURL: import.meta.env.VITE_BASE_URL,
|
||||
/**
|
||||
* 接口请求超时时间
|
||||
*/
|
||||
requestTimeout: import.meta.env.VITE_REQUEST_TIMEOUT,
|
||||
/**
|
||||
* 是否使用本地路由
|
||||
*/
|
||||
useLocalRouter: false,
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
import 'virtual:svg-icons-register';
|
||||
import '@/icons/remixicon.less';
|
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 877 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 200 B |
@ -0,0 +1,4 @@
|
||||
const fs = require('fs');
|
||||
let data = fs.readFileSync('./remixicon.less').toString();
|
||||
let res = data.match(/(?<=\.x-icon-).*?(?=:before)/g);
|
||||
fs.writeFileSync('index.json', JSON.stringify(res));
|
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<el-scrollbar class="layout-aside">
|
||||
<el-menu collapse :default-active="activeAside">
|
||||
<el-menu-item v-for="(item, index) in asideList" :key="index" :index="item.name" @click="handleClick(item)">
|
||||
<el-icon :name="item.meta.icon" size="30" />
|
||||
<p>{{ item.meta.title }}</p>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
const asideList = computed(() => store.getters['layout/asideList']);
|
||||
const activeAside = computed(() => store.state.layout.activeAside);
|
||||
|
||||
// 当前激活侧边导航改变时重设菜单列表
|
||||
watch(
|
||||
() => activeAside,
|
||||
(value) => {
|
||||
store.commit(
|
||||
'layout/setMenuList',
|
||||
unref(asideList).find((item) => item.name === unref(value))?.children || []
|
||||
);
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
const handleClick = (item) => {
|
||||
store.commit('layout/setActiveAside', item.name);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-aside {
|
||||
flex: 1;
|
||||
width: @layout-aside-width;
|
||||
height: 100%;
|
||||
background-color: @color-black;
|
||||
:deep(.el-scrollbar__view) {
|
||||
height: 100%;
|
||||
.el-menu {
|
||||
--el-menu-bg-color: transparent;
|
||||
--el-menu-text-color: @layout-aside-fc;
|
||||
--el-menu-item-height: @layout-aside-item-size;
|
||||
--el-menu-hover-bg-color: @layout-aside-fc2;
|
||||
border-right: none;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.el-menu-item {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 !important;
|
||||
border-radius: @layout-border-radius;
|
||||
width: @layout-aside-item-size;
|
||||
.x-icon {
|
||||
margin-bottom: @layout-space;
|
||||
}
|
||||
P {
|
||||
line-height: 1;
|
||||
}
|
||||
&.is-active {
|
||||
background-color: var(--el-menu-active-color);
|
||||
color: @layout-aside-fc;
|
||||
}
|
||||
+ .el-menu-item {
|
||||
margin-top: @layout-space;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<el-breadcrumb separator="/">
|
||||
<template v-for="(item, index) in breakcrumbList" :key="index">
|
||||
<el-breadcrumb-item v-if="index === breakcrumbList.length - 1">{{ item.meta.title }}</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-else :to="{ name: item.name }">
|
||||
{{ item.meta.title }}
|
||||
</el-breadcrumb-item>
|
||||
</template>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const store = useStore();
|
||||
const breakcrumbList = computed(() => store.state.layout.breakcrumbList);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.el-breadcrumb {
|
||||
font-size: 16px;
|
||||
margin-left: @layout-space-super;
|
||||
:deep(.el-breadcrumb__item) {
|
||||
.el-breadcrumb__inner {
|
||||
color: @layout-header-fc;
|
||||
a {
|
||||
color: @layout-header-fc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="layout-header">
|
||||
<div class="header-left">
|
||||
<el-icon :name="collapseMenu ? 'menu-unfold-fill' : 'menu-fold-fill'" size="16" @click="handleCollapse" />
|
||||
<LayoutBreakcrumb />
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<LayoutOperation />
|
||||
<LayoutProfile />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import LayoutBreakcrumb from './breakcrumb.vue';
|
||||
import LayoutProfile from './profile.vue';
|
||||
import LayoutOperation from './operation.vue';
|
||||
const store = useStore();
|
||||
const collapseMenu = computed(() => store.getters['layout/collapseMenu']);
|
||||
const handleCollapse = () => {
|
||||
store.commit('layout/setCollapseMenu', !unref(collapseMenu));
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: @layout-header-bgc;
|
||||
color: @layout-header-fc;
|
||||
box-shadow: @layout-shadow;
|
||||
.header-left,
|
||||
.header-right {
|
||||
height: @layout-header-height;
|
||||
padding: 0 @layout-space-large;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.x-icon {
|
||||
border-radius: @layout-border-radius;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="layout-logo">
|
||||
<el-icon name="msb" svg :size="size" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import variables from '@/styles/globalVariables.module.less';
|
||||
const size = variables.layoutLogoSize;
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-logo {
|
||||
width: @layout-aside-width;
|
||||
height: @layout-header-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: @color-black;
|
||||
.x-icon {
|
||||
border-radius: @layout-border-radius;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<el-scrollbar class="layout-main" always>
|
||||
<RouterView />
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RouterView from './view.vue';
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-main {
|
||||
margin: @layout-space-large;
|
||||
background-color: @color-white;
|
||||
border-radius: @layout-border-radius;
|
||||
box-shadow: @layout-shadow;
|
||||
> :deep(.el-scrollbar__wrap) {
|
||||
> .el-scrollbar__view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
> * {
|
||||
// display: inline-block; // 创建BFC
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: @layout-space-large;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<el-menu-item v-if="!menuItem.children?.length" :index="props.menuItem.name" :route="{ name: props.menuItem.name }">
|
||||
<el-icon :name="props.menuItem.meta.icon" size="20" />
|
||||
<p>{{ props.menuItem.meta.title }}</p>
|
||||
</el-menu-item>
|
||||
<el-sub-menu v-else :index="props.menuItem.name">
|
||||
<template #title>
|
||||
<el-icon :name="props.menuItem.meta.icon" size="20" />
|
||||
<p>{{ props.menuItem.meta.title }}</p>
|
||||
</template>
|
||||
<MenuItem v-for="(item, index) in menuItem.children" :key="index" :menu-item="item" />
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
menuItem: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.x-icon {
|
||||
width: @layout-icon;
|
||||
color: @layout-menu-ic;
|
||||
}
|
||||
.el-menu-item {
|
||||
width: 100%;
|
||||
min-width: unset;
|
||||
border-radius: @layout-border-radius;
|
||||
&:hover {
|
||||
color: @layout-menu-hover-fc;
|
||||
}
|
||||
&.is-active {
|
||||
background-color: @layout-menu-active-bgc;
|
||||
color: @layout-menu-active-fc;
|
||||
}
|
||||
+ .el-menu-item {
|
||||
margin-top: @layout-space-small;
|
||||
}
|
||||
}
|
||||
.el-sub-menu {
|
||||
border-radius: @layout-border-radius;
|
||||
&.is-opened {
|
||||
background-color: @color-ghost-black;
|
||||
> :deep(.el-sub-menu__title) {
|
||||
background-color: @layout-menu-hover-bgc;
|
||||
}
|
||||
}
|
||||
:deep(.el-sub-menu__title) {
|
||||
border-radius: @layout-border-radius;
|
||||
margin-bottom: @layout-space-small;
|
||||
}
|
||||
+ .el-menu-item {
|
||||
margin-top: @layout-space-small;
|
||||
}
|
||||
:deep(.el-menu) {
|
||||
padding: 0 !important;
|
||||
border-radius: @layout-border-radius;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<el-scrollbar class="layout-menu" :class="{ collapse: collapseMenu }">
|
||||
<div class="title">马士兵严选后台管理平台</div>
|
||||
<el-divider>{{ activeAsideName }}</el-divider>
|
||||
<el-menu unique-opened :default-active="activeMenu" @select="handleSelect">
|
||||
<MenuItem v-for="(item, index) in menuList" :key="index" :menu-item="item" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MenuItem from './menu-item.vue';
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const activeAsideName = computed(() => store.getters['layout/activeAsideName']);
|
||||
const activeAside = computed(() => store.state.layout.activeAside);
|
||||
const menuList = computed(() => store.state.layout.menuList);
|
||||
const activeMenu = computed(() => store.state.layout.activeMenu);
|
||||
const collapseMenu = computed(() => store.getters['layout/collapseMenu']);
|
||||
const handleSelect = (index) => {
|
||||
// TODO 支持外链
|
||||
router.push({ name: index });
|
||||
};
|
||||
// 菜单栏收起时自动跳转
|
||||
watch(
|
||||
() => unref(menuList),
|
||||
(value) => {
|
||||
if (value.length === 1) {
|
||||
handleSelect(value[0].name);
|
||||
} else if (value.length === 0) {
|
||||
handleSelect(unref(activeAside));
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-menu {
|
||||
flex: 1;
|
||||
width: @layout-menu-width;
|
||||
height: 100%;
|
||||
background-color: @layout-menu-bgc;
|
||||
box-shadow: @layout-shadow;
|
||||
padding: 0 @layout-space;
|
||||
transition: width 0.3s;
|
||||
&.collapse {
|
||||
width: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
:deep(*) {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
}
|
||||
:deep(*) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.title {
|
||||
width: 100%;
|
||||
height: @layout-header-height;
|
||||
line-height: @layout-header-height;
|
||||
font-size: @layout-h2;
|
||||
color: @layout-menu-active-fc;
|
||||
text-align: center;
|
||||
.text-overflow();
|
||||
}
|
||||
.el-divider {
|
||||
margin: 0 0 @layout-space-super 0;
|
||||
border-color: @color-ghost-white;
|
||||
:deep(.el-divider__text) {
|
||||
color: @layout-menu-active-fc;
|
||||
background-color: @layout-menu-bgc;
|
||||
}
|
||||
}
|
||||
:deep(.el-scrollbar__view) {
|
||||
height: 100%;
|
||||
.el-menu {
|
||||
--el-menu-bg-color: transparent;
|
||||
--el-menu-text-color: @layout-menu-fc;
|
||||
--el-menu-hover-bg-color: @layout-menu-hover-bgc;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="layout-operation">
|
||||
<el-button type="text" @click="handleFullscreen">
|
||||
<el-icon :name="isFullscreen ? 'fullscreen-exit-fill' : 'fullscreen-fill'" />
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const isFullscreen = ref(!!document.fullscreenElement);
|
||||
const handleFullscreen = () => {
|
||||
if (isFullscreen.value) {
|
||||
document.exitFullscreen();
|
||||
isFullscreen.value = false;
|
||||
} else {
|
||||
isFullscreen.value = true;
|
||||
let fn =
|
||||
document.body.requestFullscreen ||
|
||||
document.body.webkitRequestFullscreen ||
|
||||
document.body.mozRequestFullscreen ||
|
||||
document.body.msRequestFullscreen;
|
||||
document.fullscreenEnabled && fn?.call(document.body);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-operation {
|
||||
height: @layout-header-height;
|
||||
padding: 0 @layout-space-large;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.el-avatar {
|
||||
margin-right: @layout-space;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="layout-profile">
|
||||
<el-avatar :src="userInfo?.avatar" />
|
||||
<el-dropdown :opts="opts">
|
||||
<span>{{ userInfo?.nickname || userInfo?.username }}</span>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const store = useStore();
|
||||
const userInfo = computed(() => store.state.auth.userInfo);
|
||||
const opts = reactive([
|
||||
{
|
||||
label: '个人中心',
|
||||
onClick() {
|
||||
alert('个人中心');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '退出登录',
|
||||
onClick() {
|
||||
store.dispatch('auth/logout');
|
||||
},
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-profile {
|
||||
height: @layout-header-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.el-avatar {
|
||||
margin-right: @layout-space;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div class="layout-tabs">
|
||||
<el-tabs :modelValue="activeTab" type="card" class="demo-tabs" @tab-click="handleClick">
|
||||
<el-tab-pane v-for="(item, index) in tabList" :key="index" :name="item.name">
|
||||
<template #label>
|
||||
<el-icon class="tab-icon" :name="item.meta.icon" />
|
||||
<span>{{ item.meta.title }}</span>
|
||||
<el-icon
|
||||
v-if="tabList.length > 1"
|
||||
class="tab-close"
|
||||
name="close-fill"
|
||||
@click.stop="handleCloseTab(index)"
|
||||
/>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="operation">
|
||||
<el-dropdown trigger="hover" :opts="opts" icon="apps-fill">
|
||||
<span></span>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const activeTab = computed(() => store.state.layout.activeTab);
|
||||
const tabList = computed(() => store.state.layout.tabList);
|
||||
// 标签列表变化时如果激活标签不存在则跳转到第一个标签
|
||||
watch(
|
||||
() => unref(tabList),
|
||||
(value) => {
|
||||
if (!unref(activeTab) || value.findIndex((item) => item.name === unref(activeTab)) === -1) {
|
||||
router.push({ name: value[0].name });
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
const handleCloseTab = (index) => {
|
||||
store.commit('layout/closeTab', {
|
||||
index,
|
||||
});
|
||||
};
|
||||
const handleClick = (tab) => {
|
||||
router.push(unref(tabList)[tab.index].fullPath);
|
||||
};
|
||||
const opts = reactive([
|
||||
{
|
||||
label: '关闭全部',
|
||||
onClick() {
|
||||
store.commit('layout/closeTab', {});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '关闭其他',
|
||||
onClick() {
|
||||
store.commit('layout/closeTab', {
|
||||
index: unref(tabList).findIndex((item) => item.name === unref(activeTab)),
|
||||
reverse: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-tabs {
|
||||
width: 100%;
|
||||
height: @layout-tabs-height;
|
||||
padding: 0 @layout-space-large;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
box-shadow: fade(@layout-header-fc, 15%) 0 1px 5px;
|
||||
background-color: @layout-header-bgc;
|
||||
color: @layout-header-fc;
|
||||
.el-tabs {
|
||||
height: 80%;
|
||||
width: 100%;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
.el-tabs__nav-wrap {
|
||||
height: 100%;
|
||||
margin-bottom: 0;
|
||||
> * {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.el-tabs__nav-scroll {
|
||||
.el-tabs__nav {
|
||||
height: 100%;
|
||||
border: none;
|
||||
padding: 0 @layout-space-large;
|
||||
display: flex;
|
||||
.el-tabs__item {
|
||||
height: 100%;
|
||||
min-width: 100px;
|
||||
margin-right: -8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
mask-image: url('~/layouts/tabs-bgp.png');
|
||||
-webkit-mask-image: url('~/layouts/tabs-bgp.png');
|
||||
mask-size: 100% 100%;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
&:hover {
|
||||
background-color: @color-white-dark;
|
||||
.tab-close {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
&.is-active {
|
||||
background-color: @color-primary-white;
|
||||
.tab-close {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
.tab-icon {
|
||||
margin-right: @layout-space-small;
|
||||
}
|
||||
.tab-close {
|
||||
margin-left: @layout-space-small;
|
||||
border-radius: 50px;
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
transition: width 0.3s;
|
||||
&:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.operation {
|
||||
height: 80%;
|
||||
margin-left: @layout-space;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition mode="out-in" name="fade-transform">
|
||||
<keep-alive>
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="layout-container layout-default">
|
||||
<div class="layout-left">
|
||||
<LayoutLogo />
|
||||
<LayoutAside />
|
||||
</div>
|
||||
<div class="layout-center">
|
||||
<LayoutMenu />
|
||||
</div>
|
||||
<div class="layout-right">
|
||||
<LayoutHeader />
|
||||
<LayoutTabs />
|
||||
<LayoutMain />
|
||||
<LayoutFooter />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import LayoutMain from './components/main.vue';
|
||||
import LayoutLogo from './components/logo.vue';
|
||||
import LayoutAside from './components/aside.vue';
|
||||
import LayoutMenu from './components/menu.vue';
|
||||
import LayoutHeader from './components/header.vue';
|
||||
import LayoutTabs from './components/tabs.vue';
|
||||
import LayoutFooter from './components/footer.vue';
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: @color-white-dark;
|
||||
}
|
||||
.layout-default {
|
||||
display: flex;
|
||||
.layout-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: @layout-shadow;
|
||||
}
|
||||
.layout-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,17 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
|
||||
import '@/icons';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
import store from '@/store';
|
||||
app.use(store);
|
||||
|
||||
import router from '@/router';
|
||||
app.use(router);
|
||||
|
||||
import usePlugins from '@/plugins';
|
||||
usePlugins(app);
|
||||
|
||||
app.mount('#app');
|
@ -0,0 +1,16 @@
|
||||
import { ElNotification } from 'element-plus/es/components/notification/index';
|
||||
import 'element-plus/es/components/notification/style/css';
|
||||
import { ElMessage } from 'element-plus/es/components/message/index';
|
||||
import 'element-plus/es/components/message/style/css';
|
||||
import { ElMessageBox } from 'element-plus/es/components/message-box/index';
|
||||
import 'element-plus/es/components/message-box/style/css';
|
||||
import { ElLoading } from 'element-plus/es/components/loading/index';
|
||||
import 'element-plus/es/components/loading/style/css';
|
||||
// 只需要引用带有全局方法的组件,其他组件插件会自动按需引入
|
||||
export default (app) => {
|
||||
app.use(ElNotification);
|
||||
app.use(ElMessage);
|
||||
app.use(ElMessageBox);
|
||||
app.use(ElLoading);
|
||||
};
|
||||
export { ElNotification, ElMessage, ElMessageBox, ElLoading };
|
@ -0,0 +1,6 @@
|
||||
export default (app) => {
|
||||
Object.entries(import.meta.globEager('./*.js')).forEach((entry) => {
|
||||
entry[1].default?.(app);
|
||||
console.info('[plugins] loaded ' + entry[0].split('/').pop().split('.').reverse().slice(1).reverse().join());
|
||||
});
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
export default [
|
||||
{
|
||||
path: '/demo',
|
||||
name: 'Demo',
|
||||
component: () => import('@/layouts/default.vue'),
|
||||
meta: {
|
||||
title: '组件示例',
|
||||
icon: 'home-fill',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'sortable',
|
||||
name: 'SortableTableDemo',
|
||||
component: () => import('@/views/demo/sortableTableDemo.vue'),
|
||||
meta: {
|
||||
title: '拖拽排序',
|
||||
icon: 'home-fill',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
@ -0,0 +1,137 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
// 全局路由
|
||||
export const globalRoutes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/global/login.vue'),
|
||||
meta: {
|
||||
global: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
name: '404',
|
||||
component: () => import('@/views/global/404.vue'),
|
||||
meta: {
|
||||
global: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 示例模块
|
||||
import demoModule from './demo';
|
||||
export const demeRoutes = import.meta.env.DEV ? demoModule : [];
|
||||
|
||||
// 动态模块
|
||||
const dynamicRoutes = [];
|
||||
const modules = import.meta.globEager('./modules/*.js');
|
||||
Object.values(modules).forEach((mod) => {
|
||||
dynamicRoutes.push(...mod.default);
|
||||
});
|
||||
|
||||
// 本地路由
|
||||
export const routes = [
|
||||
...globalRoutes,
|
||||
{
|
||||
path: '/',
|
||||
name: 'App',
|
||||
redirect: { name: 'Home' },
|
||||
component: () => import('@/layouts/default.vue'),
|
||||
meta: {
|
||||
layout: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/home',
|
||||
name: 'Home',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '首页',
|
||||
icon: 'home-fill',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
...dynamicRoutes,
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/404',
|
||||
name: 'NotFound',
|
||||
meta: {
|
||||
global: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
import config from '@/configs';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: config.useLocalRouter ? routes : [],
|
||||
});
|
||||
|
||||
import store from '@/store';
|
||||
router.onError((error, to) => {
|
||||
console.info('[router] error', error, to);
|
||||
});
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
if (!from.matched.length) {
|
||||
store.loadCache();
|
||||
}
|
||||
if (store.state.local.token) {
|
||||
if (!store.state.auth.permission.length) {
|
||||
if (config.useLocalRouter) {
|
||||
store.commit('auth/setPermission', routes);
|
||||
} else {
|
||||
await store.dispatch('auth/getUserInfo');
|
||||
await store.dispatch('auth/getPermission');
|
||||
}
|
||||
next({ ...to, replace: true });
|
||||
} else {
|
||||
console.info(`[router] from ${from.name} to ${to.name}`);
|
||||
const deep = (route) => {
|
||||
let child = (route.children || []).find((item) => !item.meta?.hidden);
|
||||
return child?.children?.length ? deep(child?.children) : child?.name;
|
||||
};
|
||||
let childName = deep([...to.matched].pop());
|
||||
if (childName) {
|
||||
next({ name: childName });
|
||||
} else {
|
||||
next();
|
||||
store.commit('layout/setActiveAside', to.matched.find((item) => !item.meta?.layout).name);
|
||||
store.commit('layout/setActiveMenu', to.name);
|
||||
store.commit('layout/setActiveTab', to.name);
|
||||
store.commit('layout/setBreakcrumbList', to.matched);
|
||||
store.commit('layout/addTab', to);
|
||||
}
|
||||
}
|
||||
} else if (to.meta.global) {
|
||||
next();
|
||||
} else {
|
||||
next({ name: 'Login' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
export const reset = (routers) => {
|
||||
router.getRoutes().forEach((item) => {
|
||||
router.removeRoute(item.name);
|
||||
});
|
||||
routers = [
|
||||
...globalRoutes,
|
||||
...routers,
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/404',
|
||||
name: 'NotFound',
|
||||
meta: {
|
||||
global: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
routers.forEach(router.addRoute);
|
||||
console.info('[router] reset', router.getRoutes());
|
||||
};
|
@ -0,0 +1,286 @@
|
||||
export default [
|
||||
{
|
||||
path: '/education',
|
||||
name: 'Education',
|
||||
component: () => import('@/layouts/default.vue'),
|
||||
meta: {
|
||||
title: '教务教学',
|
||||
icon: 'book-open-fill',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'teacher',
|
||||
name: 'TeacherManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '老师管理',
|
||||
icon: 'user-shared-fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'student',
|
||||
name: 'StudentManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '学生管理',
|
||||
icon: 'user-received-fill',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'StudentManagementList',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '学生列表',
|
||||
icon: 'user-received-line',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'grade',
|
||||
name: 'GradeManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '年级管理',
|
||||
icon: 'account-pin-box-fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'class',
|
||||
name: 'ClassManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '班级管理',
|
||||
icon: 'account-pin-circle-fill',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'admission',
|
||||
name: 'AdmissionManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '入学',
|
||||
icon: 'award-fill',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'evaluation',
|
||||
name: 'AdmissionEvaluation',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '入学评测',
|
||||
icon: 'file-paper-2-fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'way',
|
||||
name: 'StudyWay',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '学习路线',
|
||||
icon: 'send-plane-fill',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'answer',
|
||||
name: 'Answer',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '答疑',
|
||||
icon: 'question-answer-fill',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'management',
|
||||
name: 'AnswerManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '答疑管理',
|
||||
icon: 'question-answer-fill',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'AnswerManagementList',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '答疑管理列表',
|
||||
icon: 'question-answer-line',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'invalid',
|
||||
name: 'InvalidQuestionManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '无效追问管理',
|
||||
icon: 'questionnaire-fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'lock',
|
||||
name: 'UserLockManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '用户锁定管理',
|
||||
icon: 'lock-fill',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'assign/:questionId',
|
||||
name: 'AssignAnswerTeacher',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '指派老师',
|
||||
icon: 'account-box-fill',
|
||||
hidden: true,
|
||||
activeMenu: '/education/answer/management',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'teacher',
|
||||
name: 'AnswerTeacher',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '答疑老师管理',
|
||||
icon: 'book-line',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'summary',
|
||||
name: 'AnswerTeacherSummary',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '答疑老师统计',
|
||||
icon: 'book-2-line',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'management',
|
||||
name: 'AnswerTeacherManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '答疑老师管理',
|
||||
icon: 'book-3-line',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'bind/:teacher?',
|
||||
name: 'AnswerTeacherUpdateBind',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '编辑课程绑定',
|
||||
icon: 'book-fill',
|
||||
hidden: true,
|
||||
activeMenu: '/education/answer/teacher/management',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'course',
|
||||
name: 'AnswerTeacherBindCourse',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '绑定课程管理',
|
||||
icon: 'book-fill',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'question',
|
||||
name: 'QuestionManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '题库管理',
|
||||
icon: 'brush-fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'note',
|
||||
name: 'NoteManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '笔记管理',
|
||||
icon: 'sticky-note-2-fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'research',
|
||||
name: 'CourseResearch',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '课研更新',
|
||||
icon: 'contacts-book-upload-fill',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'plan',
|
||||
name: 'CourseResearchPlan',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '课研计划',
|
||||
icon: 'contacts-book-upload-line',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'log',
|
||||
name: 'CourseResearchLog',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '课研更新日志',
|
||||
icon: 'file-copy-2-line',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'live',
|
||||
name: 'LiveManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '直播管理',
|
||||
icon: 'live-fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'material',
|
||||
name: 'MaterialManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '老师资料管理',
|
||||
icon: 'database-2-fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'shift',
|
||||
name: 'ShiftTable',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '排班表',
|
||||
icon: 'calendar-check-line',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'course',
|
||||
name: 'CourseTable',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '课程表管理',
|
||||
icon: 'calendar-todo-fill',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
@ -0,0 +1,49 @@
|
||||
export default [
|
||||
{
|
||||
path: '/system',
|
||||
name: 'SystemManagement',
|
||||
component: () => import('@/layouts/default.vue'),
|
||||
meta: {
|
||||
title: '系统管理',
|
||||
icon: 'settings-3-fill',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'log',
|
||||
name: 'LogManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '日志管理',
|
||||
icon: 'file-chart-fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'app',
|
||||
name: 'AppManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: 'APP包管理',
|
||||
icon: 'app-store-fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'notify',
|
||||
name: 'NotifyManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '通知管理',
|
||||
icon: 'alarm-warning-fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'file',
|
||||
name: 'FileManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '文件中台',
|
||||
icon: 'file-copy-fill',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
@ -0,0 +1,49 @@
|
||||
export default [
|
||||
{
|
||||
path: '/user',
|
||||
name: 'UserManagement',
|
||||
component: () => import('@/layouts/default.vue'),
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
icon: 'folder-user-fill',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'consult',
|
||||
name: 'ConsultManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '咨询管理',
|
||||
icon: 'kakao-talk-fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'course',
|
||||
name: 'CourseConsultManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '咨询课程',
|
||||
icon: 'chat-smile-2-fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'account',
|
||||
name: 'AccountManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '账号课程',
|
||||
icon: 'account-box-fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'feedback',
|
||||
name: 'FeedbackManagement',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '意见反馈',
|
||||
icon: 'feedback-fill',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
@ -0,0 +1,53 @@
|
||||
import { createStore } from 'vuex';
|
||||
|
||||
const modules = Object.fromEntries(
|
||||
Object.entries(import.meta.globEager('./**/*.js')).map((entry) => {
|
||||
let arr = entry[0].split('/').pop().split('.');
|
||||
arr.pop();
|
||||
let moduleName = _.camelCase(arr.join('-'));
|
||||
return [
|
||||
moduleName,
|
||||
{
|
||||
...entry[1].default,
|
||||
namespaced: true,
|
||||
},
|
||||
];
|
||||
})
|
||||
);
|
||||
const store = createStore({
|
||||
strict: process.env.NODE_ENV !== 'production',
|
||||
modules,
|
||||
});
|
||||
|
||||
const storeCache = () => {
|
||||
localStorage.setItem('storeCache', JSON.stringify(store.state.local));
|
||||
// sessionStorage.setItem('storeCache', JSON.stringify(store.state));
|
||||
console.info('[store] store cache');
|
||||
};
|
||||
const loadCache = () => {
|
||||
let storeCache = sessionStorage.getItem('storeCache');
|
||||
if (storeCache) {
|
||||
let init = _.cloneDeep(store.state);
|
||||
storeCache = Object.assign(init, JSON.parse(storeCache));
|
||||
sessionStorage.removeItem('storeCache');
|
||||
} else {
|
||||
storeCache = { ...store.state };
|
||||
}
|
||||
let localCache = localStorage.getItem('storeCache');
|
||||
if (localCache) {
|
||||
storeCache.local = _.cloneDeep(store.state.local);
|
||||
Object.assign(storeCache.local, JSON.parse(localCache));
|
||||
}
|
||||
store.replaceState(storeCache);
|
||||
console.info('[store] load cache');
|
||||
};
|
||||
window._open = window.open;
|
||||
window.open = function () {
|
||||
storeCache();
|
||||
window._open.apply(window, arguments);
|
||||
};
|
||||
window.addEventListener('beforeunload', storeCache);
|
||||
store.storeCache = storeCache;
|
||||
store.loadCache = loadCache;
|
||||
|
||||
export default store;
|
@ -0,0 +1,115 @@
|
||||
import { getUserInfo, getPermission, sendSmsCode, login } from '@/api/auth';
|
||||
import { ElMessage } from '@/plugins/element-plus';
|
||||
import router, { reset as resetRoutes, demeRoutes } from '@/router';
|
||||
import config from '@/configs';
|
||||
const viewComponents = import.meta.glob('../../views/**/*.vue');
|
||||
const state = () => ({
|
||||
userInfo: null,
|
||||
permission: [],
|
||||
});
|
||||
const getters = {};
|
||||
const mutations = {
|
||||
setUserInfo: (state, data) => (state.userInfo = data),
|
||||
setPermission: (state, data) => {
|
||||
const convert = (arr, parent = { path: '/', meta: {} }) => {
|
||||
return arr.map((item) => {
|
||||
let route = {
|
||||
path: item.routeUrl,
|
||||
name: item.roleCode,
|
||||
component: item.pageUrl,
|
||||
meta: {
|
||||
title: item.title,
|
||||
icon: item.icon,
|
||||
hidden: item.type === 2,
|
||||
activeMenu: item.activeMenu,
|
||||
},
|
||||
};
|
||||
if (!route.path.startsWith('/')) {
|
||||
if (!parent.path.endsWith('/')) {
|
||||
parent.path += '/';
|
||||
}
|
||||
route.path = parent.path + route.path;
|
||||
}
|
||||
route.meta.menu = route.path.replaceAll(/\/:[^?]+\?/g, '');
|
||||
let activeMenu = route.meta.activeMenu;
|
||||
if (!activeMenu && route.meta.hidden) {
|
||||
activeMenu = parent.meta.menu;
|
||||
}
|
||||
route.meta.activeMenu = activeMenu || route.meta.menu;
|
||||
if (route.component) {
|
||||
if (route.component === 'Layout') {
|
||||
route.component = () => import('@/layouts/default.vue');
|
||||
} else {
|
||||
let path = `../../views/${route.component}`;
|
||||
if (!path.endsWith('.vue')) {
|
||||
path += '.vue';
|
||||
}
|
||||
let alias = path.replace(/\.vue$/, '/index.vue');
|
||||
route.component = viewComponents[path] || viewComponents[alias];
|
||||
if (!route.component) {
|
||||
route.component = () => import('@/views/global/404.vue');
|
||||
}
|
||||
}
|
||||
}
|
||||
route.children = convert(item.children || [], route);
|
||||
route.redirect = route.children.find((item) => !item.meta.hidden)?.menu;
|
||||
return route;
|
||||
});
|
||||
};
|
||||
data = config.useLocalRouter ? data : convert(data);
|
||||
data.push(...demeRoutes);
|
||||
state.permission = data;
|
||||
resetRoutes(state.permission);
|
||||
},
|
||||
};
|
||||
const actions = {
|
||||
sms: async ({ commit }, data) => {
|
||||
let res = await sendSmsCode(data);
|
||||
if (res) {
|
||||
ElMessage.success('验证码已发送');
|
||||
commit('local/sendMessage', {}, { root: true });
|
||||
} else {
|
||||
ElMessage.error('发送验证码失败');
|
||||
}
|
||||
return res;
|
||||
},
|
||||
login: async ({ commit }, data) => {
|
||||
let res = await login(data);
|
||||
if (res) {
|
||||
ElMessage.success('登陆成功');
|
||||
commit('local/setToken', res.token, { root: true });
|
||||
router.push('/');
|
||||
} else {
|
||||
ElMessage.error('登陆失败');
|
||||
}
|
||||
return res;
|
||||
},
|
||||
logout: ({ commit }) => {
|
||||
commit('local/setToken', null, { root: true });
|
||||
router.push({ name: 'Login' });
|
||||
},
|
||||
getPermission: async ({ commit }) => {
|
||||
let res = await getPermission({ systemCode: 'msb_shop_admin' });
|
||||
if (res) {
|
||||
commit('setPermission', res);
|
||||
} else {
|
||||
ElMessage.error('加载权限信息失败');
|
||||
}
|
||||
return res;
|
||||
},
|
||||
getUserInfo: async ({ commit }) => {
|
||||
let res = await getUserInfo();
|
||||
if (res) {
|
||||
commit('setUserInfo', res);
|
||||
} else {
|
||||
ElMessage.error('加载用户信息失败');
|
||||
}
|
||||
return res;
|
||||
},
|
||||
};
|
||||
export default {
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions,
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
const state = () => ({
|
||||
count: 0,
|
||||
});
|
||||
const getters = {
|
||||
doubleCount: (state) => state.count * 2,
|
||||
};
|
||||
const mutations = {
|
||||
add: (state, num = 1) => (state.count += num),
|
||||
};
|
||||
const actions = {
|
||||
clear: ({ state, commit }) => {
|
||||
commit('add', state.count * -1);
|
||||
},
|
||||
};
|
||||
export default {
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions,
|
||||
};
|
@ -0,0 +1,58 @@
|
||||
import router from '@/router';
|
||||
const state = () => ({
|
||||
activeAside: null,
|
||||
collapseMenu: false,
|
||||
activeMenu: null,
|
||||
menuList: [],
|
||||
tabList: [],
|
||||
activeTab: null,
|
||||
breakcrumbList: [],
|
||||
});
|
||||
const getters = {
|
||||
asideList: (state, getters, rootState) => {
|
||||
const deep = (arr) =>
|
||||
arr.flatMap((item) => (item.meta?.layout ? deep(item.children || []) : item.meta?.global ? [] : item));
|
||||
return deep(rootState.auth.permission);
|
||||
},
|
||||
collapseMenu: (state) => state.collapseMenu || state.menuList.length < 2,
|
||||
activeAsideName: (state, getters) => getters.asideList.find((item) => item.name === state.activeAside)?.meta.title,
|
||||
};
|
||||
const mutations = {
|
||||
setCollapseMenu: (state, data) => (state.collapseMenu = data),
|
||||
setActiveAside: (state, data) => (state.activeAside = data),
|
||||
setMenuList: (state, data) => (state.menuList = data),
|
||||
setActiveMenu: (state, data) => (state.activeMenu = data),
|
||||
addTab: (state, data) => {
|
||||
let oldIndex = state.tabList.findIndex((item) => item.name === data.name);
|
||||
let tab = null;
|
||||
if (oldIndex !== -1) {
|
||||
tab.fullPath = data.fullPath;
|
||||
} else {
|
||||
tab = _.cloneDeep(data);
|
||||
state.tabList.push(tab);
|
||||
}
|
||||
},
|
||||
closeTab: (state, { index, reverse }) => {
|
||||
if (typeof index === 'undefined') {
|
||||
state.tabList.splice(1);
|
||||
} else {
|
||||
if (reverse) {
|
||||
let tab = state.tabList[index];
|
||||
router.push({ name: tab.name });
|
||||
state.tabList = [tab];
|
||||
} else if (state.tabList.length > 1) {
|
||||
state.tabList.splice(index, 1);
|
||||
router.push({ name: state.tabList[Math.min(index, state.tabList.length - 1)].name });
|
||||
}
|
||||
}
|
||||
},
|
||||
setActiveTab: (state, data) => (state.activeTab = data),
|
||||
setBreakcrumbList: (state, data) => (state.breakcrumbList = data),
|
||||
};
|
||||
const actions = {};
|
||||
export default {
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions,
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
const state = () => ({
|
||||
lastSendMessageTime: 0,
|
||||
token: null,
|
||||
listSettings: {},
|
||||
listPage: {},
|
||||
});
|
||||
const getters = {};
|
||||
const mutations = {
|
||||
sendMessage: (state) => (state.lastSendMessageTime = new Date().getTime()),
|
||||
setToken: (state, data) => (state.token = data),
|
||||
setListSettings: (state, data) => (state.listSettings = data),
|
||||
setListPage: (state, data) => (state.listPage = data),
|
||||
};
|
||||
const actions = {};
|
||||
export default {
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions,
|
||||
};
|
@ -0,0 +1,39 @@
|
||||
import * as api from '@/api/system/user.js';
|
||||
import { ElMessage } from '@/plugins/element-plus';
|
||||
const state = () => ({
|
||||
loading: false,
|
||||
list: [],
|
||||
total: 0,
|
||||
opts: {
|
||||
sex: [
|
||||
{ label: '男', value: 1 },
|
||||
{ label: '女', value: 0 },
|
||||
],
|
||||
},
|
||||
});
|
||||
const getters = {};
|
||||
const mutations = {
|
||||
setLoading: (state, data) => (state.loading = data),
|
||||
setList: (state, data) => (state.list = data),
|
||||
setTotal: (state, data) => (state.total = data),
|
||||
};
|
||||
const actions = {
|
||||
search: async ({ commit }, data) => {
|
||||
commit('setLoading', true);
|
||||
let res = await api.findUserList(data);
|
||||
if (res) {
|
||||
commit('setList', res.content);
|
||||
commit('setTotal', res.totalElements);
|
||||
} else {
|
||||
ElMessage.error('查询用户失败');
|
||||
commit('setList', []);
|
||||
}
|
||||
commit('setLoading', false);
|
||||
},
|
||||
};
|
||||
export default {
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions,
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
ul,
|
||||
ol {
|
||||
list-style: none;
|
||||
}
|
||||
// *:not([class^='el-']):not([class^='el-'] *) {
|
||||
*:not([class^='el-']) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
box-sizing: border-box;
|
||||
word-break: break-all;
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/* color */
|
||||
@color-black: #282c34;
|
||||
@color-white: #fff;
|
||||
@color-white-dark: #f6f8f9;
|
||||
@color-primary: #409eff;
|
||||
@color-primary-white: #e8f4ff;
|
||||
|
||||
@color-ghost-white: fade(@color-white, 10%);
|
||||
@color-ghost-black: fade(@color-black, 10%);
|
||||
|
||||
/* mixin */
|
||||
.text-overflow {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* layout */
|
||||
@layout-icon: 30px;
|
||||
@layout-h1: 30px;
|
||||
@layout-h2: 20px;
|
||||
@layout-h3: 16px;
|
||||
@layout-h4: 14px;
|
||||
@layout-border-radius: 5px;
|
||||
@layout-space: 10px;
|
||||
@layout-space-small: 5px;
|
||||
@layout-space-large: 15px;
|
||||
@layout-space-super: 20px;
|
||||
@layout-shadow: fade(@color-black, 20%) 0px 0px 5px;
|
||||
|
||||
@layout-aside-bgc: @color-black;
|
||||
@layout-aside-fc: @color-white;
|
||||
@layout-aside-fc2: lighten(@color-black, 20%);
|
||||
@layout-aside-width: 80px;
|
||||
@layout-aside-item-size: 68px;
|
||||
|
||||
@layout-menu-width: 200px;
|
||||
@layout-menu-bgc: lighten(@color-black, 20%);
|
||||
@layout-menu-fc: darken(@color-white, 20%);
|
||||
@layout-menu-ic: darken(@color-white, 20%);
|
||||
@layout-menu-hover-bgc: lighten(@color-black, 25%);
|
||||
@layout-menu-hover-fc: darken(@color-white, 10%);
|
||||
@layout-menu-active-fc: @color-white;
|
||||
@layout-menu-active-bgc: lighten(@color-black, 35%);
|
||||
|
||||
@layout-header-height: 60px;
|
||||
@layout-header-bgc: @color-white;
|
||||
@layout-header-fc: @color-black;
|
||||
|
||||
@layout-tabs-height: 50px;
|
||||
|
||||
@layout-footer-bgc: @color-white;
|
||||
@layout-footer-fc: @color-black;
|
||||
|
||||
:export {
|
||||
layoutAsideWidth: @layout-aside-width;
|
||||
layoutLogoSize: calc(@layout-header-height * 0.72);
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 90vh;
|
||||
min-height: 90vh;
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-weight: bolder;
|
||||
}
|
||||
.dot-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 98px;
|
||||
.dot {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
font-size: 64px;
|
||||
transform: rotate(45deg);
|
||||
animation: rotate 1.2s infinite linear;
|
||||
i {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-color: #1890ff;
|
||||
border-radius: 100%;
|
||||
opacity: 0.3;
|
||||
transform: scale(0.75);
|
||||
transform-origin: 50% 50%;
|
||||
animation: spin 1s infinite linear alternate;
|
||||
&:nth-child(1) {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
top: 0;
|
||||
right: 0;
|
||||
-webkit-animation-delay: 0.4s;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
-webkit-animation-delay: 0.8s;
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
&:nth-child(4) {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
-webkit-animation-delay: 1.2s;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
to {
|
||||
-webkit-transform: rotate(405deg);
|
||||
transform: rotate(405deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
const during = (func, name = 'func') => {
|
||||
let time = Date.now(),
|
||||
error = null;
|
||||
try {
|
||||
func();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
const msg = `[debug] exec ${name} during ${Date.now() - time}ms`;
|
||||
if (error) {
|
||||
msg += ' with error';
|
||||
}
|
||||
console.info(msg, error);
|
||||
};
|
||||
export default {
|
||||
during,
|
||||
};
|
@ -0,0 +1,74 @@
|
||||
import config from '@/configs';
|
||||
import store from '@/store';
|
||||
import qs from 'qs';
|
||||
import { ElMessage } from '@/plugins/element-plus';
|
||||
|
||||
const handleResponse = async ({ config, headers, data, status }) => {
|
||||
if (
|
||||
['application/octet-stream', 'application/zip'].indexOf(headers['content-type']) !== -1 ||
|
||||
config['down'] === 'file'
|
||||
) {
|
||||
return data;
|
||||
}
|
||||
let code = data.code || status;
|
||||
console.info('[api]', code, config.method, config.url, data.data);
|
||||
if (code !== 200) {
|
||||
ElMessage.error(data.msg || '服务器异常');
|
||||
switch (code) {
|
||||
case 500:
|
||||
case 501:
|
||||
break;
|
||||
case 524:
|
||||
case 525:
|
||||
case 50008:
|
||||
case 50012:
|
||||
case 50014:
|
||||
store.dispatch('auth/logout');
|
||||
break;
|
||||
case 404:
|
||||
case 9999:
|
||||
console.warn('接口9999', config);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
return data?.data;
|
||||
};
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: config.baseURL,
|
||||
timeout: config.requestTimeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
});
|
||||
|
||||
instance.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = store.state.local.token;
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
if (config.data && config.headers['Content-Type'] === 'application/x-www-form-urlencoded;charset=UTF-8') {
|
||||
config.data = qs.stringify(config.data);
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(response) => handleResponse(response),
|
||||
(error) => {
|
||||
if (!error.response) {
|
||||
ElMessage.error('服务器无响应');
|
||||
return null;
|
||||
} else {
|
||||
return handleResponse(error.response);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default instance;
|
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="box">
|
||||
<h1 class="title">404</h1>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { proxy } = getCurrentInstance();
|
||||
const store = useStore();
|
||||
|
||||
const refsForm = ref(null);
|
||||
const loading = ref(false);
|
||||
const form = reactive({
|
||||
phone: '',
|
||||
password: '',
|
||||
verifyCode: '',
|
||||
});
|
||||
const lastTime = computed(() => store.state.local.lastSendMessageTime);
|
||||
const waitTime = ref(60);
|
||||
const sendStep = ref(60);
|
||||
const rules = reactive({
|
||||
phone: [{ required: true, message: '请输入手机号码' }],
|
||||
password: [{ required: true, message: '请输入登录密码' }],
|
||||
verifyCode: [{ required: true, message: '请输入验证码' }],
|
||||
});
|
||||
|
||||
waitTime.value = Math.max(0, unref(sendStep) - Math.ceil((new Date().getTime() - unref(lastTime)) / 1000));
|
||||
setInterval(() => {
|
||||
waitTime.value = Math.max(0, unref(sendStep) - Math.ceil((new Date().getTime() - unref(lastTime)) / 1000));
|
||||
}, 1000);
|
||||
|
||||
const handleSms = async () => {
|
||||
if (form.phone) {
|
||||
loading.value = true;
|
||||
await store.dispatch('auth/sms', { phone: form.phone, type: 1 });
|
||||
loading.value = false;
|
||||
} else {
|
||||
proxy.$message.warning('请输入手机号码');
|
||||
}
|
||||
};
|
||||
const handleLogin = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await unref(refsForm).validate();
|
||||
await store.dispatch('auth/login', form);
|
||||
} catch (e) {
|
||||
console.info('取消登录', e);
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000 url('~/global/login-bgp.png') center center / 100% 100% no-repeat;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: @color-white;
|
||||
.box {
|
||||
width: 420px;
|
||||
padding: 30px;
|
||||
background: #1a2229;
|
||||
border-radius: 10px;
|
||||
.title {
|
||||
font-size: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="mask">
|
||||
<div class="box">
|
||||
<div class="title">登录</div>
|
||||
<el-form ref="refsForm" class="content" :model="form" :rules="rules" size="large">
|
||||
<el-form-item prop="phone">
|
||||
<el-input v-model="form.phone" class="ghost" placeholder="请输入手机号码" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="form.password" type="password" class="ghost" placeholder="请输入登录密码" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="verifyCode">
|
||||
<div class="flex">
|
||||
<el-input v-model="form.verifyCode" class="ghost" placeholder="请输入验证码" />
|
||||
<el-button class="ghost" :disabled="waitTime > 0" @click="handleSms" :loading="loading">
|
||||
{{ waitTime ? waitTime + 'S' : '发送验证码' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-button class="block" @click="handleLogin" :loading="loading">立即登录</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { proxy } = getCurrentInstance();
|
||||
const store = useStore();
|
||||
|
||||
const refsForm = ref(null);
|
||||
const loading = ref(false);
|
||||
const form = reactive({
|
||||
phone: '',
|
||||
password: '',
|
||||
verifyCode: '',
|
||||
});
|
||||
const lastTime = computed(() => store.state.local.lastSendMessageTime);
|
||||
const waitTime = ref(60);
|
||||
const sendStep = ref(60);
|
||||
const rules = reactive({
|
||||
phone: [{ required: true, message: '请输入手机号码' }],
|
||||
password: [{ required: true, message: '请输入登录密码' }],
|
||||
verifyCode: [{ required: true, message: '请输入验证码' }],
|
||||
});
|
||||
|
||||
waitTime.value = Math.max(0, unref(sendStep) - Math.ceil((new Date().getTime() - unref(lastTime)) / 1000));
|
||||
setInterval(() => {
|
||||
waitTime.value = Math.max(0, unref(sendStep) - Math.ceil((new Date().getTime() - unref(lastTime)) / 1000));
|
||||
}, 1000);
|
||||
|
||||
const handleSms = async () => {
|
||||
if (form.phone) {
|
||||
loading.value = true;
|
||||
await store.dispatch('auth/sms', { phone: form.phone, type: 1 });
|
||||
loading.value = false;
|
||||
} else {
|
||||
proxy.$message.warning('请输入手机号码');
|
||||
}
|
||||
};
|
||||
const handleLogin = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await unref(refsForm).validate();
|
||||
await store.dispatch('auth/login', form);
|
||||
} catch (e) {
|
||||
console.info('取消登录', e);
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mask {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #000 url('~/global/login-bgp.png') center center / 100% 100% no-repeat;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: @color-white;
|
||||
.box {
|
||||
width: 420px;
|
||||
padding: 30px;
|
||||
background: #1a2229;
|
||||
border-radius: @layout-border-radius;
|
||||
.title {
|
||||
margin-bottom: @layout-space-super;
|
||||
font-size: @layout-h1;
|
||||
text-align: center;
|
||||
}
|
||||
.content {
|
||||
.flex {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
+ .flex {
|
||||
margin-top: @layout-space;
|
||||
}
|
||||
.el-button {
|
||||
width: 30%;
|
||||
margin-left: @layout-space;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
:deep(.el-input) {
|
||||
input {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
:deep(.ghost) {
|
||||
background: none;
|
||||
* {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
.block {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
color: #ffffff;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
background: linear-gradient(0deg, #fb3a4e, #ff6272);
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1>{{ $route.name }}</h1>
|
||||
<h1>
|
||||
<el-icon name="app-store" size="30" />
|
||||
<span>马士兵严选</span>
|
||||
<el-icon name="msb" svg size="30"></el-icon>
|
||||
<el-icon name="vue" svg color="red" size="30" />
|
||||
<el-icon name="Avatar" color="red" size="30" />
|
||||
</h1>
|
||||
<p>count:{{ count }}, double count:{{ doubleCount }}</p>
|
||||
<el-button type="primary" @click="handleAdd">增加</el-button>
|
||||
<el-button type="danger" @click="handleClear">清除</el-button>
|
||||
<br />
|
||||
<el-date-picker></el-date-picker>
|
||||
{{ form }}
|
||||
<el-input v-model="form.msg" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const store = useStore();
|
||||
const count = computed(() => store.state.demo.count);
|
||||
const doubleCount = computed(() => store.getters['demo/doubleCount']);
|
||||
const handleAdd = () => {
|
||||
store.commit('demo/add');
|
||||
};
|
||||
const handleClear = () => {
|
||||
store.dispatch('demo/clear');
|
||||
};
|
||||
const form = reactive({ msg: '123' });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
height: 2000px !important;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>权限列表</div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>角色列表</div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<TableList
|
||||
v-loading="loading"
|
||||
code="DemoList"
|
||||
title="用户"
|
||||
:data="list"
|
||||
:total="total"
|
||||
:config="config"
|
||||
@search="handleSearch"
|
||||
@create="handleCreate"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<template #search>
|
||||
<el-form inline>
|
||||
<el-form-item prop="username" label="用户名">
|
||||
<el-input v-model="state.condition.username" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</TableList>
|
||||
</template>
|
||||
|
||||
<script setup lang="jsx">
|
||||
const store = useStore();
|
||||
const loading = computed(() => store.state.user.loading);
|
||||
const list = computed(() => store.state.user.list);
|
||||
const total = computed(() => store.state.user.total);
|
||||
const opts = computed(() => store.state.user.opts);
|
||||
const state = reactive({
|
||||
condition: {
|
||||
username: null,
|
||||
},
|
||||
});
|
||||
const handleSearch = (page) => {
|
||||
store.dispatch('user/search', { ...page, ...state.condition });
|
||||
};
|
||||
const handleCreate = (row) => {
|
||||
alert('create ' + row.id);
|
||||
};
|
||||
const handleUpdate = (row) => {
|
||||
alert('update ' + row.id);
|
||||
};
|
||||
const handleRemove = (rows) => {
|
||||
alert('delete ' + rows.map((item) => item.id).join(','));
|
||||
};
|
||||
const handleDetail = (row) => {
|
||||
alert('detail ' + row.id);
|
||||
};
|
||||
const handleEnabled = (row, enabled) => {
|
||||
alert((enabled ? 'enabled ' : 'disabled ') + row.id);
|
||||
};
|
||||
const config = reactive({
|
||||
columns: [
|
||||
{
|
||||
type: 'selection',
|
||||
fixed: 'left',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
label: '用户名',
|
||||
prop: 'username',
|
||||
minWidth: 160,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
label: '昵称',
|
||||
prop: 'nickname',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
label: '性别',
|
||||
slots: {
|
||||
default: ({ row }) => unref(opts).sex.find((item) => item.value === row.sex)?.label,
|
||||
},
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
label: '头像',
|
||||
slots: {
|
||||
default: ({ row }) => <ElImage src={row.avatar} alt="用户头像" />,
|
||||
},
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
label: '登录时间',
|
||||
slots: {
|
||||
default: ({ row }) => dayjs(row.loginTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
slots: {
|
||||
default: ({ row }) => (
|
||||
<ElSwitch
|
||||
modelValue={row.enabled}
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
active-value={true}
|
||||
inactive-value={false}
|
||||
onInput={(e) => handleEnabled(row, e)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
label: '操作',
|
||||
fixed: 'right',
|
||||
slots: {
|
||||
default: ({ row }) => (
|
||||
<div>
|
||||
<ElButton type="text" onClick={() => handleUpdate(row)}>
|
||||
编辑
|
||||
</ElButton>
|
||||
<ElButton type="text" onClick={() => handleRemove([row])}>
|
||||
删除
|
||||
</ElButton>
|
||||
<ElDropdown
|
||||
opts={[
|
||||
{
|
||||
label: '详情',
|
||||
onClick: () => handleDetail(row),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
width: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
@ -0,0 +1,102 @@
|
||||
import { UserConfigExport, ConfigEnv } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import AutoImport from 'unplugin-auto-import/vite';
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
|
||||
import { createStyleImportPlugin, ElementPlusResolve as ElementPlusStyleResolve } from 'vite-plugin-style-import';
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||
import globalStyle from '@originjs/vite-plugin-global-style';
|
||||
import removeConsole from 'vite-plugin-remove-console';
|
||||
import legacy from '@vitejs/plugin-legacy';
|
||||
|
||||
export default ({ command, mode }: ConfigEnv): UserConfigExport => {
|
||||
console.info('command', command);
|
||||
console.info('mode', mode);
|
||||
return {
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
open: false,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://gateway-test.mashibing.cn', // 测试地址
|
||||
// target: 'https://gateway.mashibing.cn', // 预发地址
|
||||
// target: 'https://gateway.mashibing.com', // 生产环境
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: '@',
|
||||
replacement: resolve(process.cwd(), 'src'),
|
||||
},
|
||||
{
|
||||
find: '~',
|
||||
replacement: resolve(process.cwd(), 'src/assets'),
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
createSvgIconsPlugin({
|
||||
iconDirs: [resolve(process.cwd(), 'src/icons/svg')],
|
||||
symbolId: 'icon-[dir]-[name]',
|
||||
}),
|
||||
createStyleImportPlugin({
|
||||
include: ['**/*.js', '**/*.ts', '**/*.tsx', '**/*.jsx', '**/*.vue'],
|
||||
resolves: [ElementPlusStyleResolve()],
|
||||
}),
|
||||
AutoImport({
|
||||
include: [/\.[jt]sx?$/, /\.vue\??/],
|
||||
imports: [
|
||||
'vue',
|
||||
'vuex',
|
||||
'vue-router',
|
||||
{
|
||||
axios: [
|
||||
['default', 'axios'], // import { default as axios } from 'axios',
|
||||
],
|
||||
},
|
||||
{ lodash: [['*', '_']] },
|
||||
{ dayjs: [['*', 'dayjs']] },
|
||||
],
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
filepath: './src/.eslintrc.json',
|
||||
globalsPropValue: true,
|
||||
},
|
||||
dts: 'src/auto-imports.d.ts',
|
||||
}),
|
||||
Components({
|
||||
dirs: ['src/components'],
|
||||
extensions: ['vue', 'jsx', 'tsx', 'js', 'ts'],
|
||||
deep: true,
|
||||
resolvers: [ElementPlusResolver()],
|
||||
dts: 'src/components.d.ts',
|
||||
include: [/\.tsx$/, /\.jsx$/, /\.ts$/, /\.js$/, /\.vue$/, /\.vue\?vue/],
|
||||
exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/],
|
||||
}),
|
||||
removeConsole(),
|
||||
globalStyle({
|
||||
sourcePath: 'src/styles',
|
||||
}),
|
||||
legacy({
|
||||
targets: ['defaults', 'not IE 11'],
|
||||
}),
|
||||
],
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
less: {
|
||||
javascriptEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
@ -0,0 +1,63 @@
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/vue-next/pull/3399
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ElArea: typeof import('./src/components/ElArea.vue')['default']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||
ElButton: typeof import('./src/components/extra/ElButton.vue')['default']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCascader: typeof import('./src/components/extra/ElCascader.vue')['default']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('./src/components/extra/ElCheckboxGroup.vue')['default']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDialog: typeof import('./src/components/extra/ElDialog.vue')['default']
|
||||
ElDropdown: typeof import('./src/components/extra/ElDropdown.vue')['default']
|
||||
ElEditor: typeof import('./src/components/ElEditor.vue')['default']
|
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('./src/components/extra/ElIcon.vue')['default']
|
||||
ElImage: typeof import('./src/components/extra/ElImage.vue')['default']
|
||||
ElInput: typeof import('./src/components/extra/ElInput.vue')['default']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioGroup: typeof import('./src/components/extra/ElRadioGroup.vue')['default']
|
||||
ElRate: typeof import('element-plus/es')['ElRate']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('./src/components/extra/ElSelect.vue')['default']
|
||||
ElStep: typeof import('element-plus/es')['ElStep']
|
||||
ElSteps: typeof import('element-plus/es')['ElSteps']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('./src/components/extra/ElSwitch.vue')['default']
|
||||
ElTable: typeof import('./src/components/extra/ElTable.vue')['default']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
ElTooltip: typeof import('./src/components/extra/ElTooltip.vue')['default']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
ElUploadFile: typeof import('./src/components/ElUploadFile.vue')['default']
|
||||
ElUploadImage: typeof import('./src/components/ElUploadImage.vue')['default']
|
||||
Loading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
TableList: typeof import('./src/components/TableList.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
export { }
|
@ -1,56 +1,54 @@
|
||||
kind: Deployment
|
||||
apiVersion: apps/v1
|
||||
metadata:
|
||||
labels:
|
||||
app: $IMAGES
|
||||
name: $IMAGES
|
||||
namespace: yanxuan
|
||||
spec:
|
||||
progressDeadlineSeconds: 600
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: $IMAGES
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 25%
|
||||
maxSurge: 25%
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
labels:
|
||||
app: $IMAGES
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: aliyun-docker-hub
|
||||
containers:
|
||||
- image: '$REGISTRY/$DOCKERHUB_NAMESPACE/$IMAGES:$BUILD_NUMBER'
|
||||
name: app
|
||||
ports:
|
||||
- containerPort: $JAR_PORD
|
||||
protocol: TCP
|
||||
resources:
|
||||
limits:
|
||||
cpu: '0.5'
|
||||
memory: 500Mi
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 30
|
||||
|
||||
name: $IMAGES
|
||||
namespace: yanxuan
|
||||
spec:
|
||||
progressDeadlineSeconds: 600
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: $IMAGES
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 25%
|
||||
maxSurge: 25%
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: $IMAGES
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: aliyun-docker-hub
|
||||
containers:
|
||||
- image: '$REGISTRY/$DOCKERHUB_NAMESPACE/$IMAGES:$BUILD_NUMBER'
|
||||
name: app
|
||||
ports:
|
||||
- containerPort: $JAR_PORD
|
||||
protocol: TCP
|
||||
resources:
|
||||
limits:
|
||||
cpu: '0.5'
|
||||
memory: 500Mi
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 30
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: $IMAGES
|
||||
namespace: yanxuan
|
||||
name: $IMAGES
|
||||
namespace: yanxuan
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
targetPort: 80
|
||||
selector:
|
||||
app: $IMAGES
|
||||
type: ClusterIP
|
||||
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
targetPort: 80
|
||||
selector:
|
||||
app: $IMAGES
|
||||
type: ClusterIP
|
||||
|
@ -1,17 +1,23 @@
|
||||
/*
|
||||
* @Author: ch
|
||||
* @Date: 2022-05-17 21:23:11
|
||||
* @LastEditors: ch
|
||||
* @LastEditTime: 2022-07-20 17:57:02
|
||||
* @Description: file content
|
||||
*/
|
||||
import '@/icons';
|
||||
import usePlugins from '@/plugins';
|
||||
import router from '@/router';
|
||||
import store from '@/store';
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
|
||||
import '@/icons';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
import store from '@/store';
|
||||
app.use(store);
|
||||
|
||||
import router from '@/router';
|
||||
app.use(router);
|
||||
|
||||
import usePlugins from '@/plugins';
|
||||
usePlugins(app);
|
||||
|
||||
app.mount('#app');
|
||||
|