v3.1.0 1、【新增】9种登录背景图和样式; 2、【新增】全局字体大小切换; 3、【新增】主题颜色切换; 4、【新增】移除cookie保存token,改为使用localStorage; 5、【优化】升级 ant design vue 到最新版本;

This commit is contained in:
zhuoda 2024-04-06 21:01:43 +08:00
parent 1723f2514f
commit 6a2c86d9f2
51 changed files with 2196 additions and 1359 deletions

File diff suppressed because it is too large Load Diff

View File

@ -19,33 +19,32 @@
"dependencies": { "dependencies": {
"@wangeditor/editor": "5.1.14", "@wangeditor/editor": "5.1.14",
"@wangeditor/editor-for-vue": "5.1.12", "@wangeditor/editor-for-vue": "5.1.12",
"ant-design-vue": "4.0.7", "ant-design-vue": "4.1.2",
"axios": "1.4.0", "axios": "1.6.8",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"crypto-js": "4.1.1", "crypto-js": "4.1.1",
"decimal.js": "10.3.1", "decimal.js": "10.3.1",
"diff": "5.1.0", "diff": "5.2.0",
"diff2html": "3.4.18", "diff2html": "3.4.47",
"echarts": "5.4.3", "echarts": "5.4.3",
"highlight.js": "11.8.0", "highlight.js": "11.8.0",
"js-cookie": "3.0.5",
"lodash": "4.17.21", "lodash": "4.17.21",
"lunar-javascript": "1.6.3", "lunar-javascript": "1.6.12",
"mitt": "3.0.0", "mitt": "3.0.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"pinia": "2.1.6", "pinia": "2.1.7",
"sm-crypto": "^0.3.13", "sm-crypto": "0.3.13",
"sortablejs": "1.15.0", "sortablejs": "1.15.0",
"ua-parser-js": "1.0.35", "ua-parser-js": "1.0.35",
"v-viewer": "~1.6.4", "v-viewer": "~1.6.4",
"vue": "3.3.4", "vue": "3.3.13",
"vue-i18n": "9.2.2", "vue-i18n": "9.10.2",
"vue-router": "4.2.4", "vue-router": "4.3.0",
"vue3-json-viewer": "2.2.2" "vue3-json-viewer": "2.2.2"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "4.5.0", "@vitejs/plugin-vue": "5.0.4",
"@vue/compiler-sfc": "3.3.4", "@vue/compiler-sfc": "3.4.21",
"eslint": "^8.16.0", "eslint": "^8.16.0",
"eslint-config-prettier": "~9.0.0", "eslint-config-prettier": "~9.0.0",
"eslint-plugin-prettier": "~5.0.0", "eslint-plugin-prettier": "~5.0.0",
@ -58,9 +57,9 @@
"stylelint-config-prettier": "~9.0.3", "stylelint-config-prettier": "~9.0.3",
"stylelint-config-standard": "~25.0.0", "stylelint-config-standard": "~25.0.0",
"stylelint-order": "~5.0.0", "stylelint-order": "~5.0.0",
"terser": "~5.19.2", "terser": "~5.29.2",
"vite": "5.0.0", "vite": "5.2.6",
"vue-eslint-parser": "~9.3.1" "vue-eslint-parser": "~9.4.2"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"

View File

@ -9,7 +9,29 @@
--> -->
<template> <template>
<a-config-provider :locale="antdLocale"> <a-config-provider
:locale="antdLocale"
:theme="{
algorithm: compactFlag ? theme.compactAlgorithm : theme.defaultAlgorithm,
token: {
colorPrimary: themeColors[colorIndex].primaryColor,
colorLink: themeColors[colorIndex].primaryColor,
colorLinkActive: themeColors[colorIndex].activeColor,
colorLinkHover: themeColors[colorIndex].hoverColor,
colorIcon: themeColors[colorIndex].primaryColor,
},
components: {
Button: {
colorLink: themeColors[colorIndex].primaryColor,
colorLinkActive: themeColors[colorIndex].activeColor,
colorLinkHover: themeColors[colorIndex].hoverColor,
},
Icon: {
colorIcon: themeColors[colorIndex].primaryColor,
},
},
}"
>
<!---全局loading---> <!---全局loading--->
<a-spin :spinning="spinning" tip="稍等片刻,我在拼命加载中..." size="large"> <a-spin :spinning="spinning" tip="稍等片刻,我在拼命加载中..." size="large">
<!--- 路由 --> <!--- 路由 -->
@ -23,12 +45,21 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { messages } from '/@/i18n'; import { messages } from '/@/i18n';
import { useAppConfigStore } from '/@/store/modules/system/app-config'; import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { useSpinStore } from './store/modules/system/spin'; import { useSpinStore } from '/@/store/modules/system/spin';
import { theme } from 'ant-design-vue';
import { themeColors } from '/@/theme/color.js';
const antdLocale = computed(() => messages[useAppConfigStore().language].antdLocale); const antdLocale = computed(() => messages[useAppConfigStore().language].antdLocale);
const dayjsLocale = computed(() => messages[useAppConfigStore().language].dayjsLocale); const dayjsLocale = computed(() => messages[useAppConfigStore().language].dayjsLocale);
dayjs.locale(dayjsLocale); dayjs.locale(dayjsLocale);
// loading
let spinStore = useSpinStore(); let spinStore = useSpinStore();
const spinning = computed(() => spinStore.loading); const spinning = computed(() => spinStore.loading);
//
const compactFlag = computed(() => useAppConfigStore().compactFlag);
//
const colorIndex = computed(() => {
return useAppConfigStore().colorIndex;
});
</script> </script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -16,6 +16,8 @@ export const appDefaultConfig = {
sideMenuWidth: 200, sideMenuWidth: 200,
// 菜单主题 // 菜单主题
sideMenuTheme: 'dark', sideMenuTheme: 'dark',
// 主题颜色索引
colorIndex: 0,
// 顶部菜单页面宽度 // 顶部菜单页面宽度
pageWidth: '99%', pageWidth: '99%',
// 标签页 // 标签页
@ -32,4 +34,6 @@ export const appDefaultConfig = {
websiteName: 'SmartAdmin 3.X', websiteName: 'SmartAdmin 3.X',
// 主题颜色 // 主题颜色
primaryColor: 'red', primaryColor: 'red',
// 紧凑
compactFlag: false,
}; };

View File

@ -16,6 +16,8 @@ const KEY_PREFIX = 'smart_admin_';
* localStorageKey集合 * localStorageKey集合
*/ */
export default { export default {
// 用户token
USER_TOKEN: `${KEY_PREFIX}user_token`,
// 用户信息 // 用户信息
USER_INFO: `${KEY_PREFIX}user_info`, USER_INFO: `${KEY_PREFIX}user_info`,
// 用户权限点 // 用户权限点

View File

@ -14,10 +14,12 @@ export default {
antdLocale: antd, antdLocale: antd,
dayjsLocale: dayjs, dayjsLocale: dayjs,
'setting.title': 'Setting', 'setting.title': 'Setting',
'setting.color': 'Theme Color',
'setting.menu.layout': 'Menu Layout', 'setting.menu.layout': 'Menu Layout',
'setting.menu.width': 'Menu Width', 'setting.menu.width': 'Menu Width',
'setting.menu.theme': 'Menu Theme', 'setting.menu.theme': 'Menu Theme',
'setting.page.width': 'Page Width', 'setting.page.width': 'Page Width',
'setting.compact': 'Page Compact',
'setting.bread': 'Show Bread', 'setting.bread': 'Show Bread',
'setting.pagetag': 'Show PageTag', 'setting.pagetag': 'Show PageTag',
'setting.footer': 'Show Footer', 'setting.footer': 'Show Footer',

View File

@ -14,9 +14,11 @@ export default {
antdLocale: antd, antdLocale: antd,
dayjsLocale: dayjs, dayjsLocale: dayjs,
'setting.title': '网站设置', 'setting.title': '网站设置',
'setting.color': '主题颜色',
'setting.menu.layout': '菜单布局', 'setting.menu.layout': '菜单布局',
'setting.menu.width': '菜单宽度', 'setting.menu.width': '菜单宽度',
'setting.menu.theme': '菜单主题', 'setting.menu.theme': '菜单主题',
'setting.compact': '页面紧凑',
'setting.page.width': '页面宽度', 'setting.page.width': '页面宽度',
'setting.bread': '面包屑', 'setting.bread': '面包屑',
'setting.pagetag': '标签页', 'setting.pagetag': '标签页',

View File

@ -33,7 +33,6 @@
import { computed, ref, onMounted } from 'vue'; import { computed, ref, onMounted } from 'vue';
import { loginApi } from '/src/api/system/login-api'; import { loginApi } from '/src/api/system/login-api';
import { useUserStore } from '/@/store/modules/system/user'; import { useUserStore } from '/@/store/modules/system/user';
import { clearAllCoolies } from '/@/utils/cookie-util';
import { localClear } from '/@/utils/local-util'; import { localClear } from '/@/utils/local-util';
import { smartSentry } from '/@/lib/smart-sentry'; import { smartSentry } from '/@/lib/smart-sentry';
import HeaderResetPassword from './header-reset-password-modal/index.vue'; import HeaderResetPassword from './header-reset-password-modal/index.vue';
@ -48,9 +47,7 @@
} catch (e) { } catch (e) {
smartSentry.captureError(e); smartSentry.captureError(e);
} finally { } finally {
localClear(); localClear();
clearAllCoolies();
useUserStore().logout(); useUserStore().logout();
location.reload(); location.reload();
} }

View File

@ -11,10 +11,35 @@
<template> <template>
<a-drawer :title="$t('setting.title')" placement="right" :open="visible" @close="close"> <a-drawer :title="$t('setting.title')" placement="right" :open="visible" @close="close">
<a-form layout="horizontal" :label-col="{ span: 8 }"> <a-form layout="horizontal" :label-col="{ span: 8 }">
<a-form-item label="语言/Language"> <a-form-item :label="$t('setting.color')">
<a-select v-model:value="formState.language" @change="changeLanguage" style="width: 120px"> <div style="display: flex; align-items: center">
<a-select-option v-for="item in i18nList" :key="item.value" :value="item.value">{{ item.text }}</a-select-option> <template v-for="(item, index) in themeColors">
</a-select> <div v-if="index === formState.colorIndex" class="color">
<CheckSquareFilled :style="{ color: item.primaryColor, fontSize: '22px' }" />
</div>
<div v-else @click="changeColor(index)" class="color">
<svg
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
:fill="item.primaryColor"
xmlns="http://www.w3.org/2000/svg"
width="26"
height="26"
>
<path
d="M128 160.01219c0-17.67619 14.336-32.01219 32.01219-32.01219h704c17.65181 0 31.98781 14.336 31.98781 32.01219v704c0 17.65181-14.336 31.98781-32.01219 31.98781H160.036571a31.98781 31.98781 0 0 1-32.01219-32.01219V160.036571z"
></path>
</svg>
</div>
</template>
</div>
</a-form-item>
<a-form-item :label="$t('setting.compact')">
<a-radio-group v-model:value="formState.compactFlag" button-style="solid" @change="changeCompactFlag">
<a-radio-button :value="false">默认</a-radio-button>
<a-radio-button :value="true">紧凑</a-radio-button>
</a-radio-group>
</a-form-item> </a-form-item>
<a-form-item :label="$t('setting.menu.layout')"> <a-form-item :label="$t('setting.menu.layout')">
<a-radio-group @change="changeLayout" button-style="solid" v-model:value="formState.layout"> <a-radio-group @change="changeLayout" button-style="solid" v-model:value="formState.layout">
@ -23,6 +48,12 @@
</a-radio-button> </a-radio-button>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item :label="$t('setting.menu.theme')">
<a-radio-group v-model:value="formState.sideMenuTheme" button-style="solid" @change="changeMenuTheme">
<a-radio-button value="dark">Dark</a-radio-button>
<a-radio-button value="light">Light</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item :label="$t('setting.menu.width')" v-if="formState.layout === LAYOUT_ENUM.SIDE.value"> <a-form-item :label="$t('setting.menu.width')" v-if="formState.layout === LAYOUT_ENUM.SIDE.value">
<a-input-number @change="changeSideMenuWidth" v-model:value="formState.sideMenuWidth" :min="1" /> <a-input-number @change="changeSideMenuWidth" v-model:value="formState.sideMenuWidth" :min="1" />
像素px 像素px
@ -31,11 +62,10 @@
<a-input @change="changePageWidth" v-model:value="formState.pageWidth" /> <a-input @change="changePageWidth" v-model:value="formState.pageWidth" />
像素px或者 百分比 像素px或者 百分比
</a-form-item> </a-form-item>
<a-form-item :label="$t('setting.menu.theme')"> <a-form-item label="语言/Language">
<a-radio-group v-model:value="formState.sideMenuTheme" button-style="solid" @change="changeMenuTheme"> <a-select v-model:value="formState.language" @change="changeLanguage" style="width: 120px">
<a-radio-button value="dark">Dark</a-radio-button> <a-select-option v-for="item in i18nList" :key="item.value" :value="item.value">{{ item.text }}</a-select-option>
<a-radio-button value="light">Light</a-radio-button> </a-select>
</a-radio-group>
</a-form-item> </a-form-item>
<a-form-item :label="$t('setting.bread')"> <a-form-item :label="$t('setting.bread')">
<a-switch @change="changeBreadCrumbFlag" v-model:checked="formState.breadCrumbFlag" checked-children="显示" un-checked-children="隐藏" /> <a-switch @change="changeBreadCrumbFlag" v-model:checked="formState.breadCrumbFlag" checked-children="显示" un-checked-children="隐藏" />
@ -69,6 +99,7 @@
import { useAppConfigStore } from '/@/store/modules/system/app-config'; import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { Modal } from 'ant-design-vue'; import { Modal } from 'ant-design-vue';
import { appDefaultConfig } from '/@/config/app-config'; import { appDefaultConfig } from '/@/config/app-config';
import { themeColors } from '/@/theme/color.js';
// ----------------- modal ----------------- // ----------------- modal -----------------
@ -130,10 +161,14 @@
layout: appConfigStore.layout, layout: appConfigStore.layout,
// //
pageWidth: appConfigStore.pageWidth, pageWidth: appConfigStore.pageWidth,
//
colorIndex: appConfigStore.colorIndex,
// //
sideMenuWidth: appConfigStore.sideMenuWidth, sideMenuWidth: appConfigStore.sideMenuWidth,
// //
sideMenuTheme: appConfigStore.sideMenuTheme, sideMenuTheme: appConfigStore.sideMenuTheme,
//
compactFlag: appConfigStore.compactFlag,
// //
pageTagFlag: appConfigStore.pageTagFlag, pageTagFlag: appConfigStore.pageTagFlag,
// //
@ -162,6 +197,13 @@
}); });
} }
function changeColor(index) {
formState.colorIndex = index;
appConfigStore.$patch({
colorIndex: index,
});
}
function changeSideMenuWidth(value) { function changeSideMenuWidth(value) {
appConfigStore.$patch({ appConfigStore.$patch({
sideMenuWidth: value, sideMenuWidth: value,
@ -180,6 +222,12 @@
}); });
} }
function changeCompactFlag(e) {
appConfigStore.$patch({
compactFlag: e.target.value,
});
}
function changeBreadCrumbFlag(e) { function changeBreadCrumbFlag(e) {
appConfigStore.$patch({ appConfigStore.$patch({
breadCrumbFlag: e, breadCrumbFlag: e,
@ -222,4 +270,14 @@
text-align: left; text-align: left;
z-index: 1; z-index: 1;
} }
.color {
margin-left: 8px;
display: inline;
height: 26px;
width: 26px;
display: flex;
justify-content: center;
align-items: center;
}
</style> </style>

View File

@ -34,9 +34,9 @@
<HeaderAvatar /> <HeaderAvatar />
</div> </div>
<!---帮助文档---> <!---帮助文档--->
<div class="user-space-item" @click="showHelpDoc"> <div class="user-space-item" @click="showHelpDoc" v-if="!showHelpDocFlag">
<question-circle-two-tone style="font-size: 18px; margin-right: 5px; margin-top: 5px" />
<span>帮助文档</span> <span>帮助文档</span>
<DoubleLeftOutlined v-if="!showHelpDocFlag" />
</div> </div>
<HeaderSetting ref="headerSetting" /> <HeaderSetting ref="headerSetting" />
@ -48,7 +48,8 @@
import HeaderSetting from './header-setting.vue'; import HeaderSetting from './header-setting.vue';
import HeaderMessage from './header-message.vue'; import HeaderMessage from './header-message.vue';
import { useAppConfigStore } from '/@/store/modules/system/app-config'; import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { ref } from 'vue'; import { computed, ref } from 'vue';
import { theme } from 'ant-design-vue';
// //
const headerSetting = ref(); const headerSetting = ref();
@ -67,10 +68,17 @@
useAppConfigStore().showHelpDoc(); useAppConfigStore().showHelpDoc();
} }
const showHelpDocFlag = computed(() => {
return useAppConfigStore().helpDocFlag;
});
// //
function search(){ function search() {
window.open("https://1024lab.net"); window.open('https://1024lab.net');
} }
const { useToken } = theme;
const { token } = useToken();
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -91,7 +99,7 @@
} }
.user-space-item:hover { .user-space-item:hover {
color: @primary-color; color: v-bind('token.colorPrimary');
background: @hover-bg-color; background: @hover-bg-color;
} }

View File

@ -12,7 +12,7 @@
<a-row style="border-bottom: 1px solid #eeeeee; position: relative" v-show="pageTagFlag"> <a-row style="border-bottom: 1px solid #eeeeee; position: relative" v-show="pageTagFlag">
<a-dropdown :trigger="['contextmenu']"> <a-dropdown :trigger="['contextmenu']">
<div class="smart-page-tag"> <div class="smart-page-tag">
<a-tabs style="width: 100%" :tab-position="mode" v-model:activeKey="selectedKey" size="small" @tabClick="selectTab" > <a-tabs style="width: 100%" :tab-position="mode" v-model:activeKey="selectedKey" size="small" @tabClick="selectTab">
<a-tab-pane v-for="item in tagNav" :key="item.menuName"> <a-tab-pane v-for="item in tagNav" :key="item.menuName">
<template #tab> <template #tab>
<span> <span>
@ -55,12 +55,7 @@
import { HOME_PAGE_NAME } from '/@/constants/system/home-const'; import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { useAppConfigStore } from '/@/store/modules/system/app-config'; import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { useUserStore } from '/@/store/modules/system/user'; import { useUserStore } from '/@/store/modules/system/user';
import { theme } from 'ant-design-vue';
// //
// const tagOperateWidth = ref(40);
// const tabBarStyle = {
// width: 'calc(100% - 80px)'
// }
// //
const pageTagFlag = computed(() => useAppConfigStore().$state.pageTagFlag); const pageTagFlag = computed(() => useAppConfigStore().$state.pageTagFlag);
@ -85,7 +80,7 @@
return; return;
} }
// tag // tag
let tag = tagNav.value.find((e) => e.menuName == name); let tag = tagNav.value.find((e) => e.menuName === name);
if (!tag) { if (!tag) {
router.push({ name: HOME_PAGE_NAME }); router.push({ name: HOME_PAGE_NAME });
return; return;
@ -96,7 +91,7 @@
// //
function closeByMenu(closeAll) { function closeByMenu(closeAll) {
let find = tagNav.value.find((e) => e.menuName == selectedKey.value); let find = tagNav.value.find((e) => e.menuName === selectedKey.value);
if (!find || closeAll) { if (!find || closeAll) {
closeTag(null, true); closeTag(null, true);
} else { } else {
@ -110,12 +105,12 @@
if (item && !closeAll) { if (item && !closeAll) {
let goName = HOME_PAGE_NAME; let goName = HOME_PAGE_NAME;
let goQuery = undefined; let goQuery = undefined;
if (item.fromMenuName && tagNav.value.some((e) => e.menuName == item.fromMenuName)) { if (item.fromMenuName && tagNav.value.some((e) => e.menuName === item.fromMenuName)) {
goName = item.fromMenuName; goName = item.fromMenuName;
goQuery = item.fromMenuQuery; goQuery = item.fromMenuQuery;
} else { } else {
// tag // tag
let index = tagNav.value.findIndex((e) => e.menuName == item.menuName); let index = tagNav.value.findIndex((e) => e.menuName === item.menuName);
if (index > 0) { if (index > 0) {
// tag // tag
let leftTagNav = tagNav.value[index - 1]; let leftTagNav = tagNav.value[index - 1];
@ -132,10 +127,14 @@
// tag closeTagNav // tag closeTagNav
useUserStore().closeTagNav(item ? item.menuName : null, closeAll); useUserStore().closeTagNav(item ? item.menuName : null, closeAll);
} }
const { useToken } = theme;
const { token } = useToken();
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
@smart-page-tag-operate-width: 40px; @smart-page-tag-operate-width: 40px;
@color-primary: v-bind('token.colorPrimary');
.smart-page-tag-operate { .smart-page-tag-operate {
width: @smart-page-tag-operate-width; width: @smart-page-tag-operate-width;
@ -164,7 +163,7 @@
} }
.smart-page-tag-operate:hover { .smart-page-tag-operate:hover {
color: @primary-color; color: @color-primary;
} }
.smart-page-tag { .smart-page-tag {
@ -184,7 +183,7 @@
.smart-page-tag-close { .smart-page-tag-close {
margin-left: 5px; margin-left: 5px;
font-size: 10px; font-size: 10px;
color: #8c8c8c; color: #666666;
} }
/** 覆盖 ant design vue的 tabs 样式,变小一点 **/ /** 覆盖 ant design vue的 tabs 样式,变小一点 **/
@ -203,15 +202,15 @@
} }
:deep(.ant-tabs-tab-active) { :deep(.ant-tabs-tab-active) {
background-color: #e8f4ff; background-color: #eeeeee;
.smart-page-tag-close { .smart-page-tag-close {
color: @primary-color; color: @color-primary;
} }
} }
:deep(.ant-tabs-nav .ant-tabs-tab:hover) { :deep(.ant-tabs-nav .ant-tabs-tab:hover) {
background-color: #e8f4ff; background-color: #eeeeee;
.smart-page-tag-close { .smart-page-tag-close {
color: @primary-color; color: @color-primary;
} }
} }
} }

View File

@ -90,17 +90,20 @@
z-index: 100; z-index: 100;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
cursor: pointer; cursor: pointer;
.logo-img { .logo-img {
width: 40px; width: 30px;
height: 40px; height: 30px;
} }
.title { .title {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
overflow: hidden; overflow: hidden;
word-wrap: break-word;
white-space: nowrap;
color: v-bind('theme === "light" ? "#001529": "#ffffff"'); color: v-bind('theme === "light" ? "#001529": "#ffffff"');
} }
} }

View File

@ -79,9 +79,10 @@
z-index: 21; z-index: 21;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center;
.logo-img { .logo-img {
width: 32px; width: 30px;
height: 32px; height: 30px;
} }
} }
@ -93,10 +94,11 @@
display: flex; display: flex;
cursor: pointer; cursor: pointer;
justify-content: center; justify-content: center;
align-items: center;
.logo-img { .logo-img {
width: 40px; width: 30px;
height: 40px; height: 30px;
} }
.title { .title {

View File

@ -88,8 +88,8 @@
let isLight = useAppConfigStore().$state.sideMenuTheme === 'light'; let isLight = useAppConfigStore().$state.sideMenuTheme === 'light';
return { return {
color: isLight ? '#001529' : '#FFFFFF', color: isLight ? '#001529' : '#FFFFFF',
background:isLight ? '#FFFFFF' : '#001529', background: isLight ? '#FFFFFF' : '#001529',
} };
}); });
const router = useRouter(); const router = useRouter();
@ -118,7 +118,7 @@
.logo-img { .logo-img {
display: inline-block; display: inline-block;
height: 45px; height: 30px;
vertical-align: middle; vertical-align: middle;
} }
.title { .title {
@ -161,13 +161,13 @@
color: v-bind('color.color'); color: v-bind('color.color');
} }
.user-space-item{ .user-space-item {
margin-left: 10px; margin-left: 10px;
} }
} }
} }
:deep(.ant-menu-horizontal){ :deep(.ant-menu-horizontal) {
border-bottom:0; border-bottom: 0;
} }
</style> </style>

View File

@ -9,11 +9,11 @@
*/ */
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import axios from 'axios'; import axios from 'axios';
import { clearAllCoolies, getTokenFromCookie } from '/@/utils/cookie-util'; import { localClear, localRead } from '/@/utils/local-util';
import { localClear } from '/@/utils/local-util';
import { decryptData, encryptData } from './encrypt'; import { decryptData, encryptData } from './encrypt';
import { DATA_TYPE_ENUM } from '../constants/common-const'; import { DATA_TYPE_ENUM } from '../constants/common-const';
import _ from 'lodash'; import _ from 'lodash';
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
// token的消息头 // token的消息头
const TOKEN_HEADER = 'x-access-token'; const TOKEN_HEADER = 'x-access-token';
@ -28,7 +28,7 @@ const smartAxios = axios.create({
smartAxios.interceptors.request.use( smartAxios.interceptors.request.use(
(config) => { (config) => {
// 在发送请求之前消息头加入token token // 在发送请求之前消息头加入token token
const token = getTokenFromCookie(); const token = localRead(LocalStorageKeyConst.USER_TOKEN);
if (token) { if (token) {
config.headers[TOKEN_HEADER] = token; config.headers[TOKEN_HEADER] = token;
} else { } else {
@ -73,7 +73,6 @@ smartAxios.interceptors.response.use(
if (res.code === 30007 || res.code === 30008) { if (res.code === 30007 || res.code === 30008) {
message.destroy(); message.destroy();
message.error('您没有登录,请重新登录'); message.error('您没有登录,请重新登录');
clearAllCoolies();
localClear(); localClear();
setTimeout(() => { setTimeout(() => {
location.href = '/'; location.href = '/';
@ -97,7 +96,6 @@ smartAxios.interceptors.response.use(
content: res.msg, content: res.msg,
onOk() { onOk() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
clearAllCoolies();
localClear(); localClear();
setTimeout(() => { setTimeout(() => {
location.href = '/'; location.href = '/';

View File

@ -25,8 +25,10 @@ import smartEnumPlugin from '/@/plugins/smart-enums-plugin';
import { buildRoutes, router } from '/@/router'; import { buildRoutes, router } from '/@/router';
import { store } from '/@/store'; import { store } from '/@/store';
import { useUserStore } from '/@/store/modules/system/user'; import { useUserStore } from '/@/store/modules/system/user';
import 'ant-design-vue/dist/reset.css';
import '/@/theme/index.less'; import '/@/theme/index.less';
import { getTokenFromCookie } from '/@/utils/cookie-util'; import { localRead } from '/@/utils/local-util.js';
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
/* /*
* -------------------- 着重 解释说明下main.js的初始化逻辑 begin -------------------- * -------------------- 着重 解释说明下main.js的初始化逻辑 begin --------------------
@ -82,7 +84,7 @@ function initVue() {
} }
//不需要获取用户信息、用户菜单、用户菜单动态路由直接初始化vue即可 //不需要获取用户信息、用户菜单、用户菜单动态路由直接初始化vue即可
let token = getTokenFromCookie(); let token = localRead(LocalStorageKeyConst.USER_TOKEN);
if (!token) { if (!token) {
initVue(); initVue();
} else { } else {

View File

@ -16,9 +16,9 @@ import { PAGE_PATH_404, PAGE_PATH_LOGIN } from '/@/constants/common-const';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const'; import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import SmartLayout from '../layout/index.vue'; import SmartLayout from '../layout/index.vue';
import { useUserStore } from '/@/store/modules/system/user'; import { useUserStore } from '/@/store/modules/system/user';
import { clearAllCoolies, getTokenFromCookie } from '/@/utils/cookie-util'; import { localClear, localRead } from '/@/utils/local-util';
import { localClear } from '/@/utils/local-util';
import _ from 'lodash'; import _ from 'lodash';
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
export const router = createRouter({ export const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
@ -39,9 +39,8 @@ router.beforeEach(async (to, from, next) => {
} }
// 验证登录 // 验证登录
const token = getTokenFromCookie(); const token = localRead(LocalStorageKeyConst.USER_TOKEN);
if (!token) { if (!token) {
clearAllCoolies();
localClear(); localClear();
next({ path: PAGE_PATH_LOGIN }); next({ path: PAGE_PATH_LOGIN });
return; return;

View File

@ -17,5 +17,5 @@ export const loginRouters = [
title: '登录', title: '登录',
hideInMenu: true, hideInMenu: true,
}, },
} },
]; ];

View File

@ -12,8 +12,8 @@ import { defineStore } from 'pinia';
import localKey from '/@/constants/local-storage-key-const'; import localKey from '/@/constants/local-storage-key-const';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const'; import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { MENU_TYPE_ENUM } from '/@/constants/system/menu-const'; import { MENU_TYPE_ENUM } from '/@/constants/system/menu-const';
import { getTokenFromCookie } from '/@/utils/cookie-util';
import { localClear, localRead, localSave } from '/@/utils/local-util'; import { localClear, localRead, localSave } from '/@/utils/local-util';
import LocalStorageKeyConst from '/@/constants/local-storage-key-const';
export const useUserStore = defineStore({ export const useUserStore = defineStore({
id: 'userStore', id: 'userStore',
@ -61,7 +61,7 @@ export const useUserStore = defineStore({
if (state.token) { if (state.token) {
return state.token; return state.token;
} }
return getTokenFromCookie(); return localRead(LocalStorageKeyConst.USER_TOKEN);
}, },
//是否初始化了 路由 //是否初始化了 路由
getMenuRouterInitFlag(state) { getMenuRouterInitFlag(state) {

View File

@ -0,0 +1,38 @@
export const themeColors = [
// 蓝色
{
primaryColor: '#1677ff',
activeColor: '#0958d9',
hoverColor: '#bae0ff',
},
// 紫色
{
primaryColor: '#722ED1',
activeColor: '#531dab',
hoverColor: '#9254de',
},
// 红色
{
primaryColor: '#F5222D',
activeColor: '#cf1322',
hoverColor: '#ff4d4f',
},
// 青色
{
primaryColor: '#13c2c2',
activeColor: '#08979c',
hoverColor: '#36cfc9',
},
// 粉色
{
primaryColor: '#EB2F96',
activeColor: '#c41d7f',
hoverColor: '#f759ab',
},
// 绿色
{
primaryColor: '#52C41A',
activeColor: '#389e0d',
hoverColor: '#73d13d',
},
];

View File

@ -1,29 +0,0 @@
/*
* cookie相关操作
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:58:49
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*/
import Cookies from 'js-cookie';
export const COOKIE_TOKEN_KEY = 'user_token';
export const clearAllCoolies = () => {
Cookies.remove(COOKIE_TOKEN_KEY);
};
export const getTokenFromCookie = () => {
return Cookies.get(COOKIE_TOKEN_KEY);
};
/**
* 一年后cookie过期
*
* @param token
*/
export const saveTokenToCookie = (token) => {
Cookies.set(COOKIE_TOKEN_KEY, token, { expires: 365 });
};

View File

@ -7,6 +7,7 @@
* @Email: lab1024@163.com * @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012 * @Copyright 1024创新实验室 https://1024lab.net Since 2012
*/ */
export const localSave = (key, value) => { export const localSave = (key, value) => {
localStorage.setItem(key, value); localStorage.setItem(key, value);
}; };

View File

@ -9,7 +9,7 @@
* *
--> -->
<template> <template>
<default-home-card extra="更多" icon="FireTwoTone" title="更新日志" @extraClick="onMore"> <default-home-card extra="更多" icon="FlagOutlined" title="更新日志" @extraClick="onMore">
<a-empty v-if="$lodash.isEmpty(data)" /> <a-empty v-if="$lodash.isEmpty(data)" />
<ul v-else> <ul v-else>
<template v-for="(item, index) in data" :key="index"> <template v-for="(item, index) in data" :key="index">

View File

@ -13,9 +13,9 @@
<a-card size="small"> <a-card size="small">
<template #title> <template #title>
<div class="title"> <div class="title">
<component :is="$antIcons[props.icon]" v-if="props.icon" :style="{ fontSize: '18px' }" /> <component :is="$antIcons[props.icon]" v-if="props.icon" :style="{ fontSize: '18px', color: token.colorPrimary }" />
<slot name="title"></slot> <slot name="title"></slot>
<span v-if="!$slots.title" class="smart-margin-left10">{{ props.title }}</span> <span v-if="!$slots.title" class="smart-margin-left10">{{ props.title }} </span>
</div> </div>
</template> </template>
<template v-if="props.extra" #extra> <template v-if="props.extra" #extra>
@ -27,19 +27,28 @@
</div> </div>
</template> </template>
<script setup> <script setup>
let props = defineProps({ import { theme } from 'ant-design-vue';
import { computed } from 'vue';
let props = defineProps({
icon: String, icon: String,
title: String, title: String,
extra: String, extra: String,
}); });
let emits = defineEmits(['extraClick']); let emits = defineEmits(['extraClick']);
function extraClick() { function extraClick() {
emits('extraClick'); emits('extraClick');
} }
const { useToken } = theme;
const { token } = useToken();
const color = computed(() => {
return token.colorPrimary;
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.card-container { .card-container {
background-color: #fff; background-color: #fff;
height: 100%; height: 100%;
@ -53,8 +62,8 @@ function extraClick() {
left: 0; left: 0;
width: 3px; width: 3px;
height: 30px; height: 30px;
background-color: @primary-color; background-color: v-bind('token.colorPrimary');
}
} }
} }
}
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<default-home-card icon="ProfileTwoTone" title="销量统计"> <default-home-card icon="Profile" title="销量统计">
<div class="echarts-box"> <div class="echarts-box">
<div class="category-main" id="category-main"></div> <div class="category-main" id="category-main"></div>
</div> </div>

View File

@ -7,41 +7,41 @@
* @FilePath: /smart-admin/src/views/system/home/components/gauge.vue * @FilePath: /smart-admin/src/views/system/home/components/gauge.vue
--> -->
<template> <template>
<default-home-card icon="RocketTwoTone" title="业绩完成度"> <default-home-card icon="Rocket" title="业绩完成度">
<div class="echarts-box"> <div class="echarts-box">
<div id="gauge-main" class="gauge-main"></div> <div id="gauge-main" class="gauge-main"></div>
</div> </div>
</default-home-card> </default-home-card>
</template> </template>
<script setup> <script setup>
import DefaultHomeCard from "/@/views/system/home/components/default-home-card.vue"; import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import * as echarts from "echarts"; import * as echarts from 'echarts';
import {onMounted, watch} from "vue"; import { onMounted, watch } from 'vue';
import {reactive} from "vue"; import { reactive } from 'vue';
const props = defineProps({ const props = defineProps({
percent: { percent: {
type: Number, type: Number,
default: 0 default: 0,
}, },
}); });
let option = reactive({}); let option = reactive({});
watch( watch(
() => props.percent, () => props.percent,
() => { () => {
init(); init();
} }
); );
onMounted(() => { onMounted(() => {
init(); init();
}); });
function init() { function init() {
option = { option = {
series: [ series: [
{ {
type: "gauge", type: 'gauge',
startAngle: 90, startAngle: 90,
endAngle: -270, endAngle: -270,
pointer: { pointer: {
@ -54,7 +54,7 @@ function init() {
clip: false, clip: false,
itemStyle: { itemStyle: {
borderWidth: 1, borderWidth: 1,
borderColor: "#464646", borderColor: '#464646',
}, },
}, },
axisLine: { axisLine: {
@ -77,12 +77,12 @@ function init() {
data: [ data: [
{ {
value: props.percent, value: props.percent,
name: "完成度", name: '完成度',
title: { title: {
offsetCenter: ["0%", "-10%"], offsetCenter: ['0%', '-10%'],
}, },
detail: { detail: {
offsetCenter: ["0%", "20%"], offsetCenter: ['0%', '20%'],
}, },
}, },
], ],
@ -91,22 +91,21 @@ function init() {
}, },
detail: { detail: {
fontSize: 16, fontSize: 16,
color: "auto", color: 'auto',
formatter: "{value}%", formatter: '{value}%',
}, },
}, },
], ],
}; };
let chartDom = document.getElementById("gauge-main"); let chartDom = document.getElementById('gauge-main');
if (chartDom) { if (chartDom) {
let myChart = echarts.init(chartDom); let myChart = echarts.init(chartDom);
option && myChart.setOption(option); option && myChart.setOption(option);
} }
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.echarts-box { .echarts-box {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -115,5 +114,5 @@ function init() {
height: 260px; height: 260px;
background: #fff; background: #fff;
} }
} }
</style> </style>

View File

@ -1,20 +1,20 @@
<template> <template>
<default-home-card icon="FundTwoTone" title="代码提交量"> <default-home-card icon="BarChartOutlined" title="代码提交量">
<div class="echarts-box"> <div class="echarts-box">
<div class="gradient-main" id="gradient-main"></div> <div class="gradient-main" id="gradient-main"></div>
</div> </div>
</default-home-card> </default-home-card>
</template> </template>
<script setup> <script setup>
import DefaultHomeCard from "/@/views/system/home/components/default-home-card.vue"; import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import {onMounted} from "vue"; import { onMounted } from 'vue';
onMounted(() => { onMounted(() => {
init(); init();
}); });
function init(){ function init() {
let option = { let option = {
color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'], color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],
tooltip: { tooltip: {
@ -22,30 +22,30 @@ function init(){
axisPointer: { axisPointer: {
type: 'cross', type: 'cross',
label: { label: {
backgroundColor: '#6a7985' backgroundColor: '#6a7985',
} },
} },
}, },
legend: { legend: {
data: ['罗伊', '佩弦', '开云', '清野', '飞叶'] data: ['罗伊', '佩弦', '开云', '清野', '飞叶'],
}, },
grid: { grid: {
left: '3%', left: '3%',
right: '4%', right: '4%',
bottom: '3%', bottom: '3%',
containLabel: true containLabel: true,
}, },
xAxis: [ xAxis: [
{ {
type: 'category', type: 'category',
boundaryGap: false, boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
} },
], ],
yAxis: [ yAxis: [
{ {
type: 'value' type: 'value',
} },
], ],
series: [ series: [
{ {
@ -54,7 +54,7 @@ function init(){
stack: 'Total', stack: 'Total',
smooth: true, smooth: true,
lineStyle: { lineStyle: {
width: 0 width: 0,
}, },
showSymbol: false, showSymbol: false,
areaStyle: { areaStyle: {
@ -62,18 +62,18 @@ function init(){
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ {
offset: 0, offset: 0,
color: 'rgb(128, 255, 165)' color: 'rgb(128, 255, 165)',
}, },
{ {
offset: 1, offset: 1,
color: 'rgb(1, 191, 236)' color: 'rgb(1, 191, 236)',
} },
]) ]),
}, },
emphasis: { emphasis: {
focus: 'series' focus: 'series',
}, },
data: [140, 232, 101, 264, 90, 340, 250] data: [140, 232, 101, 264, 90, 340, 250],
}, },
{ {
name: '佩弦', name: '佩弦',
@ -81,7 +81,7 @@ function init(){
stack: 'Total', stack: 'Total',
smooth: true, smooth: true,
lineStyle: { lineStyle: {
width: 0 width: 0,
}, },
showSymbol: false, showSymbol: false,
areaStyle: { areaStyle: {
@ -89,18 +89,18 @@ function init(){
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ {
offset: 0, offset: 0,
color: 'rgb(0, 221, 255)' color: 'rgb(0, 221, 255)',
}, },
{ {
offset: 1, offset: 1,
color: 'rgb(77, 119, 255)' color: 'rgb(77, 119, 255)',
} },
]) ]),
}, },
emphasis: { emphasis: {
focus: 'series' focus: 'series',
}, },
data: [120, 282, 111, 234, 220, 340, 310] data: [120, 282, 111, 234, 220, 340, 310],
}, },
{ {
name: '开云', name: '开云',
@ -108,7 +108,7 @@ function init(){
stack: 'Total', stack: 'Total',
smooth: true, smooth: true,
lineStyle: { lineStyle: {
width: 0 width: 0,
}, },
showSymbol: false, showSymbol: false,
areaStyle: { areaStyle: {
@ -116,18 +116,18 @@ function init(){
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ {
offset: 0, offset: 0,
color: 'rgb(55, 162, 255)' color: 'rgb(55, 162, 255)',
}, },
{ {
offset: 1, offset: 1,
color: 'rgb(116, 21, 219)' color: 'rgb(116, 21, 219)',
} },
]) ]),
}, },
emphasis: { emphasis: {
focus: 'series' focus: 'series',
}, },
data: [320, 132, 201, 334, 190, 130, 220] data: [320, 132, 201, 334, 190, 130, 220],
}, },
{ {
name: '清野', name: '清野',
@ -135,7 +135,7 @@ function init(){
stack: 'Total', stack: 'Total',
smooth: true, smooth: true,
lineStyle: { lineStyle: {
width: 0 width: 0,
}, },
showSymbol: false, showSymbol: false,
areaStyle: { areaStyle: {
@ -143,18 +143,18 @@ function init(){
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ {
offset: 0, offset: 0,
color: 'rgb(255, 0, 135)' color: 'rgb(255, 0, 135)',
}, },
{ {
offset: 1, offset: 1,
color: 'rgb(135, 0, 157)' color: 'rgb(135, 0, 157)',
} },
]) ]),
}, },
emphasis: { emphasis: {
focus: 'series' focus: 'series',
}, },
data: [220, 402, 231, 134, 190, 230, 120] data: [220, 402, 231, 134, 190, 230, 120],
}, },
{ {
name: '飞叶', name: '飞叶',
@ -162,42 +162,42 @@ function init(){
stack: 'Total', stack: 'Total',
smooth: true, smooth: true,
lineStyle: { lineStyle: {
width: 0 width: 0,
}, },
showSymbol: false, showSymbol: false,
label: { label: {
show: true, show: true,
position: 'top' position: 'top',
}, },
areaStyle: { areaStyle: {
opacity: 0.8, opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ {
offset: 0, offset: 0,
color: 'rgb(255, 191, 0)' color: 'rgb(255, 191, 0)',
}, },
{ {
offset: 1, offset: 1,
color: 'rgb(224, 62, 76)' color: 'rgb(224, 62, 76)',
} },
]) ]),
}, },
emphasis: { emphasis: {
focus: 'series' focus: 'series',
}, },
data: [220, 302, 181, 234, 210, 290, 150] data: [220, 302, 181, 234, 210, 290, 150],
} },
] ],
}; };
let chartDom = document.getElementById("gradient-main"); let chartDom = document.getElementById('gradient-main');
if (chartDom) { if (chartDom) {
let myChart = echarts.init(chartDom); let myChart = echarts.init(chartDom);
option && myChart.setOption(option); option && myChart.setOption(option);
} }
} }
</script> </script>
<style lang='less' scoped> <style lang="less" scoped>
.echarts-box { .echarts-box {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -206,5 +206,5 @@ function init(){
height: 300px; height: 300px;
background: #fff; background: #fff;
} }
} }
</style> </style>

View File

@ -1,27 +1,27 @@
<template> <template>
<default-home-card icon="PieChartTwoTone" title="加班统计"> <default-home-card icon="PieChartOutlined" title="加班统计">
<div class="echarts-box"> <div class="echarts-box">
<div class="pie-main" id="pie-main"></div> <div class="pie-main" id="pie-main"></div>
</div> </div>
</default-home-card> </default-home-card>
</template> </template>
<script setup> <script setup>
import DefaultHomeCard from "/@/views/system/home/components/default-home-card.vue"; import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import {onMounted} from "vue"; import { onMounted } from 'vue';
onMounted(() => { onMounted(() => {
init(); init();
}); });
function init(){ function init() {
let option = { let option = {
tooltip: { tooltip: {
trigger: 'item' trigger: 'item',
}, },
legend: { legend: {
top: '5%', top: '5%',
left: 'center' left: 'center',
}, },
series: [ series: [
{ {
@ -32,40 +32,40 @@ function init(){
itemStyle: { itemStyle: {
borderRadius: 10, borderRadius: 10,
borderColor: '#fff', borderColor: '#fff',
borderWidth: 2 borderWidth: 2,
}, },
label: { label: {
show: false, show: false,
position: 'center' position: 'center',
}, },
emphasis: { emphasis: {
label: { label: {
show: true, show: true,
fontSize: '40', fontSize: '40',
fontWeight: 'bold' fontWeight: 'bold',
} },
}, },
labelLine: { labelLine: {
show: false show: false,
}, },
data: [ data: [
{ value: 10, name: '初晓' }, { value: 10, name: '初晓' },
{ value: 8, name: '善逸' }, { value: 8, name: '善逸' },
{ value: 3, name: '胡克' }, { value: 3, name: '胡克' },
{ value: 1, name: '罗伊' }, { value: 1, name: '罗伊' },
] ],
} },
] ],
}; };
let chartDom = document.getElementById("pie-main"); let chartDom = document.getElementById('pie-main');
if (chartDom) { if (chartDom) {
let myChart = echarts.init(chartDom); let myChart = echarts.init(chartDom);
option && myChart.setOption(option); option && myChart.setOption(option);
} }
} }
</script> </script>
<style lang='less' scoped> <style lang="less" scoped>
.echarts-box { .echarts-box {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -74,5 +74,5 @@ function init(){
height: 260px; height: 260px;
background: #fff; background: #fff;
} }
} }
</style> </style>

View File

@ -9,7 +9,7 @@
* *
--> -->
<template> <template>
<default-home-card icon="SmileTwoTone" title="联系我们"> <default-home-card icon="SmileOutlined" title="联系我们">
<div class="app-qr-box"> <div class="app-qr-box">
<div class="app-qr"> <div class="app-qr">
<img :src="zhuoda" /> <img :src="zhuoda" />

View File

@ -1,17 +1,12 @@
<template> <template>
<default-home-card <default-home-card :extra="`${editFlag ? '完成' : '编辑'}`" icon="ThunderboltTwoTone" title="快捷入口" @extraClick="editFlag = !editFlag">
:extra="`${editFlag ? '完成' : '编辑'}`"
icon="ThunderboltTwoTone"
title="快捷入口"
@extraClick="editFlag = !editFlag"
>
<div class="quick-entry-list"> <div class="quick-entry-list">
<a-row> <a-row>
<a-col v-for="(item,index) in quickEntry" :key="index" span="4"> <a-col v-for="(item, index) in quickEntry" :key="index" span="4">
<div class="quick-entry" @click="turnToPage(item.path)"> <div class="quick-entry" @click="turnToPage(item.path)">
<div class="icon"> <div class="icon">
<component :is='$antIcons[item.icon]' :style="{ fontSize:'30px'}"/> <component :is="$antIcons[item.icon]" :style="{ fontSize: '30px' }" />
<close-circle-outlined v-if="editFlag" class="delete-icon" @click="deleteQuickEntry(index)"/> <close-circle-outlined v-if="editFlag" class="delete-icon" @click="deleteQuickEntry(index)" />
</div> </div>
<span class="entry-title">{{ item.title }}</span> <span class="entry-title">{{ item.title }}</span>
</div> </div>
@ -19,32 +14,33 @@
<a-col v-if="editFlag && quickEntry.length < maxCount" span="4"> <a-col v-if="editFlag && quickEntry.length < maxCount" span="4">
<div class="add-quick-entry" @click="addHomeQuickEntry"> <div class="add-quick-entry" @click="addHomeQuickEntry">
<div class="add-icon"> <div class="add-icon">
<plus-outlined :style="{ fontSize:'30px'}"/> <plus-outlined :style="{ fontSize: '30px' }" />
</div> </div>
</div> </div>
</a-col> </a-col>
</a-row> </a-row>
</div> </div>
</default-home-card> </default-home-card>
<HomeQuickEntryModal ref="homeQuickEntryModal" @addQuickEntry="addQuickEntry"/> <HomeQuickEntryModal ref="homeQuickEntryModal" @addQuickEntry="addQuickEntry" />
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from "vue"; import { onMounted, ref } from 'vue';
import {router} from "/@/router"; import { router } from '/@/router';
import HomeQuickEntryModal from './home-quick-entry-modal.vue' import HomeQuickEntryModal from './home-quick-entry-modal.vue';
import localKey from '/@/constants/local-storage-key-const'; import localKey from '/@/constants/local-storage-key-const';
import {localRead, localSave} from '/@/utils/local-util'; import { localRead, localSave } from '/@/utils/local-util';
import _ from "lodash"; import _ from 'lodash';
import InitQuickEntryList from './init-quick-entry-list'; import InitQuickEntryList from './init-quick-entry-list';
import DefaultHomeCard from "/@/views/system/home/components/default-home-card.vue"; import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import { theme } from 'ant-design-vue';
//---------------- -------------------- //---------------- --------------------
onMounted(() => { onMounted(() => {
initQuickEntry(); initQuickEntry();
}) });
let quickEntry = ref([]) let quickEntry = ref([]);
function initQuickEntry() { function initQuickEntry() {
let quickEntryJson = localRead(localKey.HOME_QUICK_ENTRY); let quickEntryJson = localRead(localKey.HOME_QUICK_ENTRY);
if (!quickEntryJson) { if (!quickEntryJson) {
quickEntry.value = _.cloneDeep(InitQuickEntryList); quickEntry.value = _.cloneDeep(InitQuickEntryList);
@ -56,40 +52,43 @@ function initQuickEntry() {
return; return;
} }
quickEntry.value = quickEntryList; quickEntry.value = quickEntryList;
} }
// //
function turnToPage(path) { function turnToPage(path) {
if (editFlag.value) { if (editFlag.value) {
return; return;
} }
router.push({path}); router.push({ path });
} }
//---------------- -------------------- //---------------- --------------------
let editFlag = ref(false); let editFlag = ref(false);
let maxCount = ref(6); let maxCount = ref(6);
// //
function deleteQuickEntry(index) { function deleteQuickEntry(index) {
quickEntry.value.splice(index, 1) quickEntry.value.splice(index, 1);
localSave(localKey.HOME_QUICK_ENTRY, JSON.stringify(quickEntry.value)); localSave(localKey.HOME_QUICK_ENTRY, JSON.stringify(quickEntry.value));
} }
// //
let homeQuickEntryModal = ref(); let homeQuickEntryModal = ref();
function addHomeQuickEntry() { function addHomeQuickEntry() {
homeQuickEntryModal.value.showModal(); homeQuickEntryModal.value.showModal();
} }
function addQuickEntry(row) { function addQuickEntry(row) {
quickEntry.value.push(row); quickEntry.value.push(row);
localSave(localKey.HOME_QUICK_ENTRY, JSON.stringify(quickEntry.value)); localSave(localKey.HOME_QUICK_ENTRY, JSON.stringify(quickEntry.value));
} }
const { useToken } = theme;
const { token } = useToken();
</script> </script>
<style lang='less' scoped> <style lang="less" scoped>
.quick-entry-list { .quick-entry-list {
height: 100%; height: 100%;
.quick-entry { .quick-entry {
@ -110,12 +109,12 @@ function addQuickEntry(row) {
} }
&:hover { &:hover {
background-color: #F0FFFF; background-color: #f0ffff;
} }
.delete-icon { .delete-icon {
position: absolute; position: absolute;
color: #F08080; color: #f08080;
top: -5px; top: -5px;
right: -5px; right: -5px;
} }
@ -133,17 +132,17 @@ function addQuickEntry(row) {
border: 1px dashed #d9d9d9; border: 1px dashed #d9d9d9;
border-radius: 2px; border-radius: 2px;
cursor: pointer; cursor: pointer;
transition: border-color .3s; transition: border-color 0.3s;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #A9A9A9; color: #a9a9a9;
&:hover { &:hover {
border-color: @primary-color; border-color: v-bind('token.colorPrimary');
color: @primary-color; color: v-bind('token.colorPrimary');
}
} }
} }
} }
}
</style> </style>

View File

@ -9,8 +9,8 @@
* *
--> -->
<template> <template>
<default-home-card icon="StarTwoTone" title="已办待办"> <default-home-card icon="Star" title="已办待办">
<div style="height: 280px;"> <div style="height: 280px">
<div class="center column"> <div class="center column">
<a-space direction="vertical" style="width: 100%"> <a-space direction="vertical" style="width: 100%">
<div v-for="(item, index) in toDoList" :key="index" :class="['to-do', { done: item.doneFlag }]"> <div v-for="(item, index) in toDoList" :key="index" :class="['to-do', { done: item.doneFlag }]">

View File

@ -9,7 +9,7 @@
* *
--> -->
<template> <template>
<default-home-card extra="更多" icon="SoundTwoTone" title="通知公告" @extraClick="onMore"> <default-home-card extra="更多" icon="SoundOutlined" title="通知公告" @extraClick="onMore">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div class="content-wrapper"> <div class="content-wrapper">
<a-empty v-if="$lodash.isEmpty(data)" /> <a-empty v-if="$lodash.isEmpty(data)" />
@ -37,10 +37,6 @@
import { noticeApi } from '/@/api/business/oa/notice-api'; import { noticeApi } from '/@/api/business/oa/notice-api';
import { smartSentry } from '/@/lib/smart-sentry'; import { smartSentry } from '/@/lib/smart-sentry';
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue'; import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import { theme } from 'ant-design-vue';
const { useToken } = theme;
const { token } = useToken();
const colorPrimary = token.value.colorPrimary;
const props = defineProps({ const props = defineProps({
noticeTypeId: { noticeTypeId: {
@ -110,7 +106,6 @@
overflow: hidden; overflow: hidden;
word-break: break-all; word-break: break-all;
margin-right: 5px; margin-right: 5px;
color: v-bind(colorPrimary);
} }
.time { .time {

View File

@ -1,7 +1,7 @@
.login-container { .login-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
background: url(/@/assets/images/login/login-bg.jpg) no-repeat center; background: url(/@/assets/images/login/login-bg.png) no-repeat center;
background-size: cover; background-size: cover;
display: flex; display: flex;
align-items: center; align-items: center;
@ -20,6 +20,7 @@
border-radius: 0px 12px 12px 0px; border-radius: 0px 12px 12px 0px;
padding: 34px 42px; padding: 34px 42px;
position: relative; position: relative;
border: solid 1px #efefef;
} }
.login-qr { .login-qr {
position: absolute; position: absolute;
@ -160,7 +161,7 @@
.btn { .btn {
width: 350px; width: 350px;
height: 50px; height: 50px;
background: #1890ff; background: #1748FD;
border-radius: 4px; border-radius: 4px;
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
@ -193,14 +194,14 @@
margin: 0 19px; margin: 0 19px;
} }
.login-type { .login-type {
padding: 0 50px; padding: 0 5px;
margin-top: 25px; margin-top: 25px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
> img { > img {
width: 22px; width: 30px;
height: 22px; height: 30px;
} }
} }
} }

View File

@ -86,38 +86,37 @@
<p class="line"></p> <p class="line"></p>
</div> </div>
<div class="login-type"> <div class="login-type">
<img :src="aliLogin" /> <img src="/@/assets/images/login/wechat-icon.png" />
<img :src="qqLogin" /> <img src="/@/assets/images/login/ali-icon.png" />
<img :src="googleLogin" /> <img src="/@/assets/images/login/douyin-icon.png" />
<img :src="weiboLogin" /> <img src="/@/assets/images/login/qq-icon.png" />
<img src="/@/assets/images/login/weibo-icon.png" />
<img src="/@/assets/images/login/feishu-icon.png" />
<img src="/@/assets/images/login/google-icon.png" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { message } from 'ant-design-vue'; import { message, notification, Button } from 'ant-design-vue';
import { onMounted, onUnmounted, reactive, ref } from 'vue'; import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { loginApi } from '/@/api/system/login-api'; import { loginApi } from '/@/api/system/login-api';
import { SmartLoading } from '/@/components/framework/smart-loading'; import { SmartLoading } from '/@/components/framework/smart-loading';
import { LOGIN_DEVICE_ENUM } from '/@/constants/system/login-device-const'; import { LOGIN_DEVICE_ENUM } from '/@/constants/system/login-device-const';
import { useUserStore } from '/@/store/modules/system/user'; import { useUserStore } from '/@/store/modules/system/user';
import { saveTokenToCookie } from '/@/utils/cookie-util';
import gongzhonghao from '/@/assets/images/1024lab/1024lab-gzh.jpg'; import gongzhonghao from '/@/assets/images/1024lab/1024lab-gzh.jpg';
import zhuoda from '/@/assets/images/1024lab/zhuoda-wechat.jpg'; import zhuoda from '/@/assets/images/1024lab/zhuoda-wechat.jpg';
import loginQR from '/@/assets/images/login/login-qr.png'; import loginQR from '/@/assets/images/login/login-qr.png';
import gzh from '/@/assets/images/1024lab/gzh.jpg'; import gzh from '/@/assets/images/1024lab/gzh.jpg';
import aliLogin from '/@/assets/images/login/ali-icon.png';
import googleLogin from '/@/assets/images/login/google-icon.png';
import qqLogin from '/@/assets/images/login/qq-icon.png';
import weiboLogin from '/@/assets/images/login/weibo-icon.png';
import { buildRoutes } from '/@/router/index'; import { buildRoutes } from '/@/router/index';
import { smartSentry } from '/@/lib/smart-sentry'; import { smartSentry } from '/@/lib/smart-sentry';
import { encryptData } from '/@/lib/encrypt'; import { encryptData } from '/@/lib/encrypt';
import { h } from 'vue';
import { localSave } from '/@/utils/local-util.js';
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
//--------------------- --------------------------------- //--------------------- ---------------------------------
@ -145,6 +144,25 @@
onLogin(); onLogin();
} }
}; };
notification['success']({
message: '温馨提示',
description: 'SmartAdmin 提供 9种 登录背景风格哦!',
duration: null,
onClick: () => {},
btn: () =>
h(
Button,
{
type: 'primary',
target: '_blank',
size: 'small',
href: 'https://smartadmin.vip/views/v3/front/Login.html',
onClick: () => {},
},
{ default: () => '去看看' }
),
});
}); });
onUnmounted(() => { onUnmounted(() => {
@ -162,7 +180,7 @@
}); });
const res = await loginApi.login(encryptPasswordForm); const res = await loginApi.login(encryptPasswordForm);
stopRefrestCaptchaInterval(); stopRefrestCaptchaInterval();
saveTokenToCookie(res.data.token ? res.data.token : ''); localSave(LocalStorageKeyConst.USER_TOKEN, res.data.token ? res.data.token : '');
message.success('登录成功'); message.success('登录成功');
//pinia //pinia
useUserStore().setUserLoginInfo(res.data); useUserStore().setUserLoginInfo(res.data);

View File

@ -0,0 +1,196 @@
.login-container {
width: 100%;
height: 100%;
background: url(/@/assets/images/login/login-bg.png) no-repeat center;
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
.box-item {
width: 444px;
height: 570px;
&.desc {
background: #ffffff;
border-radius: 12px 0px 0px 12px;
box-shadow: 0px 16px 73px 8px rgba(203, 203, 203, 0.2);
padding: 23px 25px;
}
&.login {
background: #ffffff;
border-radius: 0px 12px 12px 0px;
padding: 34px 42px;
position: relative;
}
.login-qr {
position: absolute;
top: 0;
right: 0;
width: 66px;
height: 66px;
}
.welcome {
background: url(/@/assets/images/login/left-bg1.png) no-repeat center;
background-size: cover;
height: 100%;
border-radius: 8px;
box-shadow: 0px 6px 20px 0px rgba(33,47,117,0.10);
padding-top: 35px;
p{
color: #333333;
line-height: 25px;
letter-spacing: 0.26px;
text-align: center;
font-weight: 700;
font-size: 26px;
}
.sub-welcome{
color: #333333;
line-height: 25px;
letter-spacing: 0.26px;
font-weight: 700;
font-size: 20px;
}
}
.app-qr-box {
display: flex;
align-items: center;
justify-content: space-around;
margin-top: 20px;
.app-qr {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
width: 50%;
> img {
width: 112px;
height: 112px;
}
.qr-desc {
display: flex;
align-items: center;
margin-top: 11px;
font-size: 12px;
font-weight: 500;
text-align: center;
color: #ffffff;
> img {
width: 15px;
height: 18px;
margin-right: 9px;
}
}
.qr-desc-marquee {
width: 100%;
display: flex;
align-items: center;
margin-top: 11px;
font-size: 12px;
font-weight: 500;
text-align: center;
color: #ffffff;
@keyframes marquee {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-30%);
}
}
.marquee {
flex: 1;
overflow: hidden;
span {
display: inline-block;
width: 100%;
white-space: nowrap;
animation: marquee 5s linear infinite;
}
}
}
}
}
.login-title {
font-size: 20px;
text-align: center;
color: #1e1e1e;
margin-bottom: 35px;
}
.login-form {
.captcha-input {
width: 60%;
}
.captcha-img {
cursor: pointer;
}
}
.ant-input,
.ant-input-affix-wrapper {
height: 44px;
border: 1px solid #ededed;
border-radius: 4px;
}
.eye-box {
position: absolute;
right: 15px;
top: 10px;
.eye-icon {
width: 20px;
height: 20px;
cursor: pointer;
}
}
.btn {
width: 350px;
height: 50px;
background: #1748FD;
border-radius: 4px;
font-size: 16px;
font-weight: 700;
text-align: center;
color: #ffffff;
line-height: 50px;
cursor: pointer;
}
}
.more {
margin-top: 30px;
.title-box {
display: flex;
align-items: center;
justify-content: center;
> p {
margin-bottom: 0;
}
}
.line {
width: 114px;
height: 1px;
background: #e6e6e6;
}
.title {
font-size: 14px;
font-weight: 500;
color: #a1aebe;
margin: 0 19px;
}
.login-type {
padding: 0 5px;
margin-top: 25px;
display: flex;
align-items: center;
justify-content: space-between;
> img {
width: 30px;
height: 30px;
}
}
}
}

View File

@ -0,0 +1,175 @@
<!--
* 登录
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<div class="login-container">
<div class="box-item desc">
<div class="welcome">
<p>欢迎登录 SmartAdmin V3</p>
<p class="sub-welcome">高质量代码的快速开发平台</p>
</div>
</div>
<div class="box-item login">
<img class="login-qr" :src="loginQR" />
<div class="login-title">账号登录</div>
<a-form ref="formRef" class="login-form" :model="loginForm" :rules="rules">
<a-form-item name="loginName">
<a-input v-model:value.trim="loginForm.loginName" placeholder="请输入用户名" />
</a-form-item>
<a-form-item name="password">
<a-input-password
v-model:value="loginForm.password"
autocomplete="on"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码:至少三种字符,最小 8 位"
/>
</a-form-item>
<a-form-item name="captchaCode">
<a-input class="captcha-input" v-model:value.trim="loginForm.captchaCode" placeholder="请输入验证码" />
<img class="captcha-img" :src="captchaBase64Image" @click="getCaptcha" />
</a-form-item>
<a-form-item>
<a-checkbox v-model:checked="rememberPwd">记住密码</a-checkbox>
<span> ( 账号admin, 密码123456)</span>
</a-form-item>
<a-form-item>
<div class="btn" @click="onLogin">登录</div>
</a-form-item>
</a-form>
<div class="more">
<div class="title-box">
<p class="line"></p>
<p class="title">其他方式登录</p>
<p class="line"></p>
</div>
<div class="login-type">
<img src="/@/assets/images/login/wechat-icon.png" />
<img src="/@/assets/images/login/ali-icon.png" />
<img src="/@/assets/images/login/douyin-icon.png" />
<img src="/@/assets/images/login/qq-icon.png" />
<img src="/@/assets/images/login/weibo-icon.png" />
<img src="/@/assets/images/login/feishu-icon.png" />
<img src="/@/assets/images/login/google-icon.png" />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { message } from 'ant-design-vue';
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { loginApi } from '/@/api/system/login-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { LOGIN_DEVICE_ENUM } from '/@/constants/system/login-device-const';
import { useUserStore } from '/@/store/modules/system/user';
import loginQR from '/@/assets/images/login/login-qr.png';
import { buildRoutes } from '/@/router/index';
import { smartSentry } from '/@/lib/smart-sentry';
import { encryptData } from '/@/lib/encrypt';
import { localSave } from '/@/utils/local-util.js';
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
//--------------------- ---------------------------------
const loginForm = reactive({
loginName: 'admin',
password: '',
captchaCode: '',
captchaUuid: '',
loginDevice: LOGIN_DEVICE_ENUM.PC.value,
});
const rules = {
loginName: [{ required: true, message: '用户名不能为空' }],
password: [{ required: true, message: '密码不能为空' }],
captchaCode: [{ required: true, message: '验证码不能为空' }],
};
const showPassword = ref(false);
const router = useRouter();
const formRef = ref();
const rememberPwd = ref(false);
onMounted(() => {
document.onkeyup = (e) => {
if (e.keyCode == 13) {
onLogin();
}
};
});
onUnmounted(() => {
document.onkeyup = null;
});
//
async function onLogin() {
formRef.value.validate().then(async () => {
try {
SmartLoading.show();
//
let encryptPasswordForm = Object.assign({}, loginForm, {
password: encryptData(loginForm.password),
});
const res = await loginApi.login(encryptPasswordForm);
stopRefrestCaptchaInterval();
localSave(LocalStorageKeyConst.USER_TOKEN, res.data.token ? res.data.token : '');
message.success('登录成功');
//pinia
useUserStore().setUserLoginInfo(res.data);
//
buildRoutes();
router.push('/home');
} catch (e) {
if (e.data && e.data.code !== 0) {
loginForm.captchaCode = '';
getCaptcha();
}
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
});
}
//--------------------- ---------------------------------
const captchaBase64Image = ref('');
async function getCaptcha() {
try {
let captchaResult = await loginApi.getCaptcha();
captchaBase64Image.value = captchaResult.data.captchaBase64Image;
loginForm.captchaUuid = captchaResult.data.captchaUuid;
beginRefrestCaptchaInterval(captchaResult.data.expireSeconds);
} catch (e) {
console.log(e);
}
}
let refrestCaptchaInterval = null;
function beginRefrestCaptchaInterval(expireSeconds) {
if (refrestCaptchaInterval === null) {
refrestCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
}
}
function stopRefrestCaptchaInterval() {
if (refrestCaptchaInterval != null) {
clearInterval(refrestCaptchaInterval);
refrestCaptchaInterval = null;
}
}
onMounted(getCaptcha);
</script>
<style lang="less" scoped>
@import './login.less';
</style>

View File

@ -0,0 +1,204 @@
.login-container {
width: 100%;
height: 100%;
background: url(/@/assets/images/login/login-bg.png) no-repeat center;
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
.box-item {
width: 444px;
height: 570px;
&.desc {
background: #ffffff;
border-radius: 12px 0px 0px 12px;
box-shadow: 0px 16px 73px 8px rgba(203, 203, 203, 0.2);
padding: 23px 25px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
&.login {
background: #ffffff;
border-radius: 0px 12px 12px 0px;
padding: 34px 42px;
position: relative;
}
.login-qr {
position: absolute;
top: 0;
right: 0;
width: 66px;
height: 66px;
}
.welcome {
padding-top: 35px;
p{
color: #333333;
line-height: 25px;
letter-spacing: 0.26px;
text-align: center;
font-weight: 700;
font-size: 26px;
}
.sub-welcome{
padding: 10px;
font-size: 18px;
font-weight: 700;
text-align: center;
color: #ffffff;
line-height: 25px;
letter-spacing: 0.26px;
opacity: 0.96;
background: #1748fd;
border-radius: 22px 22px 2px 22px;
}
}
.welcome-img{
width: 350px;
height: 350px;
}
.app-qr-box {
display: flex;
align-items: center;
justify-content: space-around;
margin-top: 20px;
.app-qr {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
width: 50%;
> img {
width: 112px;
height: 112px;
}
.qr-desc {
display: flex;
align-items: center;
margin-top: 11px;
font-size: 12px;
font-weight: 500;
text-align: center;
color: #ffffff;
> img {
width: 15px;
height: 18px;
margin-right: 9px;
}
}
.qr-desc-marquee {
width: 100%;
display: flex;
align-items: center;
margin-top: 11px;
font-size: 12px;
font-weight: 500;
text-align: center;
color: #ffffff;
@keyframes marquee {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-30%);
}
}
.marquee {
flex: 1;
overflow: hidden;
span {
display: inline-block;
width: 100%;
white-space: nowrap;
animation: marquee 5s linear infinite;
}
}
}
}
}
.login-title {
font-size: 20px;
text-align: center;
color: #1e1e1e;
margin-bottom: 35px;
}
.login-form {
.captcha-input {
width: 60%;
}
.captcha-img {
cursor: pointer;
}
}
.ant-input,
.ant-input-affix-wrapper {
height: 44px;
border: 1px solid #ededed;
border-radius: 4px;
}
.eye-box {
position: absolute;
right: 15px;
top: 10px;
.eye-icon {
width: 20px;
height: 20px;
cursor: pointer;
}
}
.btn {
width: 350px;
height: 50px;
background: #1748FD;
border-radius: 4px;
font-size: 16px;
font-weight: 700;
text-align: center;
color: #ffffff;
line-height: 50px;
cursor: pointer;
}
}
.more {
margin-top: 30px;
.title-box {
display: flex;
align-items: center;
justify-content: center;
> p {
margin-bottom: 0;
}
}
.line {
width: 114px;
height: 1px;
background: #e6e6e6;
}
.title {
font-size: 14px;
font-weight: 500;
color: #a1aebe;
margin: 0 19px;
}
.login-type {
padding: 0 5px;
margin-top: 25px;
display: flex;
align-items: center;
justify-content: space-between;
> img {
width: 30px;
height: 30px;
}
}
}
}

View File

@ -0,0 +1,176 @@
<!--
* 登录
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<div class="login-container">
<div class="box-item desc">
<div class="welcome">
<p>欢迎登录 SmartAdmin V3</p>
<p class="sub-welcome">高质量代码的快速开发平台</p>
</div>
<img class="welcome-img" src="/@/assets/images/login/left-bg2.png" />
</div>
<div class="box-item login">
<img class="login-qr" :src="loginQR" />
<div class="login-title">账号登录</div>
<a-form ref="formRef" class="login-form" :model="loginForm" :rules="rules">
<a-form-item name="loginName">
<a-input v-model:value.trim="loginForm.loginName" placeholder="请输入用户名" />
</a-form-item>
<a-form-item name="password">
<a-input-password
v-model:value="loginForm.password"
autocomplete="on"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码:至少三种字符,最小 8 位"
/>
</a-form-item>
<a-form-item name="captchaCode">
<a-input class="captcha-input" v-model:value.trim="loginForm.captchaCode" placeholder="请输入验证码" />
<img class="captcha-img" :src="captchaBase64Image" @click="getCaptcha" />
</a-form-item>
<a-form-item>
<a-checkbox v-model:checked="rememberPwd">记住密码</a-checkbox>
<span> ( 账号admin, 密码123456)</span>
</a-form-item>
<a-form-item>
<div class="btn" @click="onLogin">登录</div>
</a-form-item>
</a-form>
<div class="more">
<div class="title-box">
<p class="line"></p>
<p class="title">其他方式登录</p>
<p class="line"></p>
</div>
<div class="login-type">
<img src="/@/assets/images/login/wechat-icon.png" />
<img src="/@/assets/images/login/ali-icon.png" />
<img src="/@/assets/images/login/douyin-icon.png" />
<img src="/@/assets/images/login/qq-icon.png" />
<img src="/@/assets/images/login/weibo-icon.png" />
<img src="/@/assets/images/login/feishu-icon.png" />
<img src="/@/assets/images/login/google-icon.png" />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { message } from 'ant-design-vue';
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { loginApi } from '/@/api/system/login-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { LOGIN_DEVICE_ENUM } from '/@/constants/system/login-device-const';
import { useUserStore } from '/@/store/modules/system/user';
import loginQR from '/@/assets/images/login/login-qr.png';
import { buildRoutes } from '/@/router/index';
import { smartSentry } from '/@/lib/smart-sentry';
import { encryptData } from '/@/lib/encrypt';
import { localSave } from '/@/utils/local-util.js';
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
//--------------------- ---------------------------------
const loginForm = reactive({
loginName: 'admin',
password: '',
captchaCode: '',
captchaUuid: '',
loginDevice: LOGIN_DEVICE_ENUM.PC.value,
});
const rules = {
loginName: [{ required: true, message: '用户名不能为空' }],
password: [{ required: true, message: '密码不能为空' }],
captchaCode: [{ required: true, message: '验证码不能为空' }],
};
const showPassword = ref(false);
const router = useRouter();
const formRef = ref();
const rememberPwd = ref(false);
onMounted(() => {
document.onkeyup = (e) => {
if (e.keyCode == 13) {
onLogin();
}
};
});
onUnmounted(() => {
document.onkeyup = null;
});
//
async function onLogin() {
formRef.value.validate().then(async () => {
try {
SmartLoading.show();
//
let encryptPasswordForm = Object.assign({}, loginForm, {
password: encryptData(loginForm.password),
});
const res = await loginApi.login(encryptPasswordForm);
stopRefrestCaptchaInterval();
localSave(LocalStorageKeyConst.USER_TOKEN, res.data.token ? res.data.token : '');
message.success('登录成功');
//pinia
useUserStore().setUserLoginInfo(res.data);
//
buildRoutes();
router.push('/home');
} catch (e) {
if (e.data && e.data.code !== 0) {
loginForm.captchaCode = '';
getCaptcha();
}
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
});
}
//--------------------- ---------------------------------
const captchaBase64Image = ref('');
async function getCaptcha() {
try {
let captchaResult = await loginApi.getCaptcha();
captchaBase64Image.value = captchaResult.data.captchaBase64Image;
loginForm.captchaUuid = captchaResult.data.captchaUuid;
beginRefrestCaptchaInterval(captchaResult.data.expireSeconds);
} catch (e) {
console.log(e);
}
}
let refrestCaptchaInterval = null;
function beginRefrestCaptchaInterval(expireSeconds) {
if (refrestCaptchaInterval === null) {
refrestCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
}
}
function stopRefrestCaptchaInterval() {
if (refrestCaptchaInterval != null) {
clearInterval(refrestCaptchaInterval);
refrestCaptchaInterval = null;
}
}
onMounted(getCaptcha);
</script>
<style lang="less" scoped>
@import './login.less';
</style>