初始化处理

This commit is contained in:
ChenYi 2025-05-27 13:54:28 +08:00
parent 039eeb6670
commit cbde1b330f
373 changed files with 22584 additions and 7481 deletions

View File

@ -2,5 +2,5 @@ ports:
- port: 5555 - port: 5555
onOpen: open-preview onOpen: open-preview
tasks: tasks:
- init: corepack enable && pnpm install - init: npm i -g corepack && pnpm install
command: pnpm run dev:play command: pnpm run dev:play

View File

@ -1,20 +0,0 @@
export default {
'*.md': ['prettier --cache --ignore-unknown --write'],
'*.vue': [
'prettier --write',
'eslint --cache --fix',
'stylelint --fix --allow-empty-input',
],
'*.{js,jsx,ts,tsx}': [
'prettier --cache --ignore-unknown --write',
'eslint --cache --fix',
],
'*.{scss,less,styl,html,vue,css}': [
'prettier --cache --ignore-unknown --write',
'stylelint --fix --allow-empty-input',
],
'package.json': ['prettier --cache --write'],
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
'prettier --cache --write--parser json',
],
};

View File

@ -1 +1 @@
20.14.0 22.1.0

2
.npmrc
View File

@ -1,5 +1,5 @@
registry = "https://registry.npmmirror.com" registry = "https://registry.npmmirror.com"
public-hoist-pattern[]=husky public-hoist-pattern[]=lefthook
public-hoist-pattern[]=eslint public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier public-hoist-pattern[]=prettier
public-hoist-pattern[]=prettier-plugin-tailwindcss public-hoist-pattern[]=prettier-plugin-tailwindcss

20
.vscode/settings.json vendored
View File

@ -14,7 +14,7 @@
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.detectIndentation": false, "editor.detectIndentation": false,
"editor.cursorBlinking": "expand", "editor.cursorBlinking": "expand",
"editor.largeFileOptimizations": false, "editor.largeFileOptimizations": true,
"editor.accessibilitySupport": "off", "editor.accessibilitySupport": "off",
"editor.cursorSmoothCaretAnimation": "on", "editor.cursorSmoothCaretAnimation": "on",
"editor.guides.bracketPairs": "active", "editor.guides.bracketPairs": "active",
@ -91,6 +91,7 @@
"**/bower_components": true, "**/bower_components": true,
"**/.turbo": true, "**/.turbo": true,
"**/.idea": true, "**/.idea": true,
"**/.vitepress": true,
"**/tmp": true, "**/tmp": true,
"**/.git": true, "**/.git": true,
"**/.svn": true, "**/.svn": true,
@ -112,6 +113,8 @@
"**/yarn.lock": true "**/yarn.lock": true
}, },
"typescript.tsserver.exclude": ["**/node_modules", "**/dist", "**/.turbo"],
// search // search
"search.searchEditor.singleClickBehaviour": "peekDefinition", "search.searchEditor.singleClickBehaviour": "peekDefinition",
"search.followSymlinks": false, "search.followSymlinks": false,
@ -216,12 +219,23 @@
"*.env": "$(capture).env.*", "*.env": "$(capture).env.*",
"README.md": "README*,CHANGELOG*,LICENSE,CNAME", "README.md": "README*,CHANGELOG*,LICENSE,CNAME",
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json", "package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json", "eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
"tailwind.config.mjs": "postcss.*" "tailwind.config.mjs": "postcss.*"
}, },
"commentTranslate.hover.enabled": false, "commentTranslate.hover.enabled": false,
"commentTranslate.multiLineMerge": true, "commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true, "vue.server.hybridMode": true,
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"oxc.enable": false "oxc.enable": false,
"cSpell.words": [
"archiver",
"axios",
"dotenv",
"isequal",
"jspm",
"napi",
"nolebase",
"rollup",
"vitest"
]
} }

View File

@ -1,4 +1,4 @@
FROM node:22-alpine AS builder FROM node:20-slim AS builder
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
ENV NODE_OPTIONS=--max-old-space-size=8192 ENV NODE_OPTIONS=--max-old-space-size=8192

View File

@ -1,5 +0,0 @@
# 应用标题
VITE_APP_TITLE=采集端综合管理
# 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=abp-vnext-pro-vben5-antd

View File

@ -1,169 +0,0 @@
/**
* 使 adapter/form 使便使
* vben-formvben-modalvben-drawer 使,
*/
import type { Component, SetupContext } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import { h } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
AutoComplete,
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Divider,
Input,
InputNumber,
InputPassword,
Mentions,
notification,
Radio,
RadioGroup,
RangePicker,
Rate,
Select,
Space,
Switch,
Textarea,
TimePicker,
TreeSelect,
Upload,
} from 'ant-design-vue';
const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',
) => {
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
return h(component, { ...props, ...attrs, placeholder }, slots);
};
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'DefaultButton'
| 'Divider'
| 'IconPicker'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'Mentions'
| 'PrimaryButton'
| 'Radio'
| 'RadioGroup'
| 'RangePicker'
| 'Rate'
| 'Select'
| 'Space'
| 'Switch'
| 'Textarea'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiSelect: (props, { attrs, slots }) => {
return h(
ApiComponent,
{
placeholder: $t('ui.placeholder.select'),
...props,
...attrs,
component: Select,
loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value',
},
slots,
);
},
ApiTreeSelect: (props, { attrs, slots }) => {
return h(
ApiComponent,
{
placeholder: $t('ui.placeholder.select'),
...props,
...attrs,
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
slots,
);
},
AutoComplete,
Checkbox,
CheckboxGroup,
DatePicker,
// 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'default' }, slots);
},
Divider,
IconPicker: (props, { attrs, slots }) => {
return h(
IconPicker,
{ iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs },
slots,
);
},
Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'primary' }, slots);
},
Radio,
RadioGroup,
RangePicker,
Rate,
Select: withDefaultPlaceholder(Select, 'select'),
Space,
Switch,
Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload,
};
// 将组件注册到全局共享状态中
globalShareState.setComponents(components);
// 定义全局共享状态中的消息提示
globalShareState.defineMessage({
// 复制成功消息提示
copyPreferencesSuccess: (title, content) => {
notification.success({
description: content,
message: title,
placement: 'bottomRight',
});
},
});
}
export { initComponentAdapter };

View File

@ -1,102 +0,0 @@
import { useUserStore } from '@vben/stores';
import { message as Message } from 'ant-design-vue';
import axios from 'axios';
import { $t } from '#/locales';
import { antdLocale } from '#/locales/index';
import { useAuthStore } from '#/store';
const api = axios.create({
baseURL: import.meta.env.DEV
? '/proxy/'
: import.meta.env.VITE_APP_API_ADDRESS,
timeout: 1000 * 60,
responseType: 'blob', // 设置响应数据类型为blob
});
api.interceptors.request.use((request) => {
// 全局拦截请求发送前提交的参数
const userStore = useUserStore();
const authStore = useAuthStore();
const token = userStore.userInfo?.token;
// 设置请求头
if (request.headers) {
request.headers.__tenant = userStore.tenant?.tenantId;
request.headers['accept-language'] = antdLocale.value.locale;
}
// 如果token过期则跳转到登录页面
if (token && userStore.checkUserLoginExpire()) {
authStore.logout();
return Promise.reject($t('common.mesage401'));
}
// 设置请求头
if (request.headers) {
request.headers.Authorization = `Bearer ${token}`;
}
return request;
});
api.interceptors.response.use(
(response) => {
/**
*
* { status: 1, error: '', data: '' }
* status 1 0
* error
*/
if (response.data.status === 1) {
if (response.data.error !== '') {
// 错误提示
Message.error(response.data.error);
return Promise.reject(response.data);
}
} else {
// useUserStore().logout()
}
return Promise.resolve(response);
},
(error) => {
let message = error.message;
if (message === 'Network Error') {
message = $t('common.mesage500');
} else if (message.includes('timeout')) {
message = $t('common.timeOut');
} else
switch (error.status) {
case 400: {
message = error.response.data.error?.validationErrors[0].message;
break;
}
case 401: {
message = $t('common.mesage401');
// useUserStore().logout()
break;
}
case 403: {
message = $t('common.mesage403');
break;
}
case 500: {
message = error.response.data.error?.message;
break;
}
default: {
if (message.includes('Request failed with status code')) {
message = $t('common.mesage500');
}
}
}
Message.error(message);
return Promise.reject(error);
},
);
export default api;

View File

@ -1,114 +0,0 @@
import { useUserStore } from '@vben/stores';
import { message as Message } from 'ant-design-vue';
import { $t } from '#/locales';
import { antdLocale } from '#/locales/index';
import { useAuthStore } from '#/store';
import { client } from '../api-client/services.gen';
client.setConfig({
baseURL: import.meta.env.DEV
? '/proxy/'
: import.meta.env.VITE_APP_API_ADDRESS,
timeout: 1000 * 60,
responseType: 'json',
throwOnError: true,
});
client.instance.interceptors.request.use((request) => {
// 全局拦截请求发送前提交的参数
const userStore = useUserStore();
const token = userStore.userInfo?.token;
// 设置请求头
if (request.headers) {
request.headers.__tenant = userStore.tenant?.tenantId;
// todo vben5 没有提供统一获取当前语言的方式
request.headers['accept-language'] = antdLocale.value.locale;
}
// 如果token过期则跳转到登录页面
if (
request.url !== undefined &&
request.url.includes('/api/app/account/login')
) {
return request;
}
if (token && userStore.checkUserLoginExpire()) {
const authStore = useAuthStore();
authStore.logout();
Message.warn($t('common.mesage401'));
return Promise.reject($t('common.mesage401'));
}
// 设置请求头
if (request.headers) {
request.headers.Authorization = `Bearer ${token}`;
}
return request;
});
client.instance.interceptors.response.use(
(response) => {
/**
*
* { status: 1, error: '', data: '' }
* status 1 0
* error
*/
if (response.data.status === 1) {
if (response.data.error !== '') {
// 错误提示
Message.error(response.data.error);
return Promise.reject(response.data);
}
} else {
// useUserStore().logout()
}
return Promise.resolve(response);
},
(error) => {
let message = error.message;
if (message === 'Network Error') {
message = $t('common.mesage500');
} else if (message.includes('timeout')) {
message = $t('common.timeOut');
} else
switch (error.status) {
case 400: {
message = error.response.data.error?.validationErrors[0].message;
break;
}
case 401: {
message = $t('common.mesage401');
// useUserStore().logout()
break;
}
case 403: {
message = $t('common.mesage403');
break;
}
case 500: {
message = error.response.data.error?.message;
break;
}
default: {
if (message.includes('Request failed with status code')) {
message = $t('common.mesage500');
}
}
}
Message.error(message);
return Promise.reject(error);
},
);
export default client;

File diff suppressed because one or more lines are too long

View File

@ -1,39 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useAntdDesignTokens } from '@vben/hooks';
import { preferences, usePreferences } from '@vben/preferences';
import { App, ConfigProvider, theme } from 'ant-design-vue';
import { antdLocale } from '#/locales';
defineOptions({ name: 'App' });
const { isDark } = usePreferences();
const { tokens } = useAntdDesignTokens();
const tokenTheme = computed(() => {
const algorithm = isDark.value
? [theme.darkAlgorithm]
: [theme.defaultAlgorithm];
// antd
if (preferences.app.compact) {
algorithm.push(theme.compactAlgorithm);
}
return {
algorithm,
token: tokens,
};
});
</script>
<template>
<ConfigProvider :locale="antdLocale" :theme="tokenTheme">
<App>
<RouterView />
</App>
</ConfigProvider>
</template>

View File

@ -1,4 +0,0 @@
export { createLoading } from './src/createLoading';
export { default as Loading } from './src/Loading.vue';
export { useLoading } from './src/useLoading';

View File

@ -1,78 +0,0 @@
<script lang="ts" setup>
import type { PropType } from 'vue';
import { Spin } from 'ant-design-vue';
import { SizeEnum } from './typing';
defineOptions({ name: 'Loading' });
defineProps({
tip: {
type: String as PropType<string>,
default: '',
},
size: {
type: String as PropType<SizeEnum>,
default: SizeEnum.LARGE,
validator: (v: SizeEnum): boolean => {
return [SizeEnum.DEFAULT, SizeEnum.LARGE, SizeEnum.SMALL].includes(v);
},
},
absolute: {
type: Boolean as PropType<boolean>,
default: false,
},
loading: {
type: Boolean as PropType<boolean>,
default: false,
},
background: {
type: String as PropType<string>,
},
theme: {
type: String as PropType<'dark' | 'light'>,
},
});
</script>
<template>
<section
v-show="loading"
:class="{ absolute, [`${theme}`]: !!theme }"
:style="[background ? `background-color: ${background}` : '']"
class="full-loading"
>
<Spin v-bind="$attrs" :size="size" :spinning="loading" :tip="tip" />
</section>
</template>
<style lang="less" scoped>
.full-loading {
display: flex;
position: fixed;
z-index: 200;
top: 0;
left: 0;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background-color: #f0f2f566;
&.absolute {
position: absolute;
z-index: 300;
top: 0;
left: 0;
}
}
html[data-theme='dark'] {
.full-loading:not(.light) {
background-color: rgba(0, 0, 0, 0.5);
}
}
.full-loading.dark {
background-color: rgba(0, 0, 0, 0.5);
}
</style>

View File

@ -1,84 +0,0 @@
import type { LoadingProps } from './typing';
import { createVNode, defineComponent, h, reactive, render } from 'vue';
import Loading from './Loading.vue';
// 创建一个加载组件的函数
export function createLoading(
props?: Partial<LoadingProps>,
target?: HTMLElement,
wait = false,
) {
let vm: any = null;
const data = reactive({
tip: '',
loading: true,
...props,
});
// 定义加载组件的包装
const LoadingWrap = defineComponent({
render() {
return h(Loading, { ...data });
},
});
vm = createVNode(LoadingWrap);
let container: any = null;
// 根据wait参数决定何时渲染Loading组件
if (wait) {
setTimeout(() => {
render(vm, (container = document.createElement('div')));
}, 0);
} else {
render(vm, (container = document.createElement('div')));
}
// 关闭加载组件的函数
function close() {
if (vm?.el && vm.el.parentNode) {
vm.el.remove();
}
}
// 打开加载组件并将其附加到目标元素上的函数
function open(target: HTMLElement = document.body) {
if (!vm || !vm.el) {
return;
}
target.append(vm.el as HTMLElement);
}
// 销毁加载组件的函数
function destroy() {
container && render(null, container);
container = vm = null;
}
// 如果提供了目标元素,则打开加载组件
if (target) {
open(target);
}
// 返回加载组件的实例及控制方法
return {
vm,
close,
open,
destroy,
setTip: (tip: string) => {
data.tip = tip;
},
setLoading: (loading: boolean) => {
data.loading = loading;
},
get loading() {
return data.loading;
},
get $el() {
return vm?.el as HTMLElement;
},
};
}

View File

@ -1,14 +0,0 @@
export enum SizeEnum {
DEFAULT = 'default',
LARGE = 'large',
SMALL = 'small',
}
export interface LoadingProps {
tip: string;
size: SizeEnum;
absolute: boolean;
loading: boolean;
background: string;
theme: 'dark' | 'light';
}

View File

@ -1,61 +0,0 @@
import type { LoadingProps } from './typing';
import type { Ref } from 'vue';
import { unref } from 'vue';
import { tryOnUnmounted } from '@vueuse/core';
import { createLoading } from './createLoading';
export interface UseLoadingOptions {
target?: any;
props?: Partial<LoadingProps>;
}
interface Fn {
(): void;
}
export function useLoading(
props: Partial<LoadingProps>,
): [Fn, Fn, (arg0: string) => void];
export function useLoading(
opt: Partial<UseLoadingOptions>,
): [Fn, Fn, (arg0: string) => void];
export function useLoading(
opt: Partial<LoadingProps> | Partial<UseLoadingOptions>,
): [Fn, Fn, (arg0: string) => void] {
let props: Partial<LoadingProps>;
let target: HTMLElement | Ref<any> = document.body;
if (Reflect.has(opt, 'target') || Reflect.has(opt, 'props')) {
const options = opt as Partial<UseLoadingOptions>;
props = options.props || {};
target = options.target || document.body;
} else {
props = opt as Partial<LoadingProps>;
}
const instance = createLoading(props, undefined, false);
const open = (): void => {
const t = unref(target as Ref<any>);
if (!t) return;
instance.open(t);
};
const close = (): void => {
instance.close();
};
const setTip = (tip: string) => {
instance.setTip(tip);
};
tryOnUnmounted(() => {
instance.destroy();
});
return [open, close, setTip];
}

View File

@ -1,51 +0,0 @@
<script setup lang="ts">
import { computed, h } from 'vue';
import { IconifyIcon as VbenIcon } from '@vben/icons';
const props = defineProps({
icon: {
type: String,
default: '',
},
size: {
type: [String, Number],
default: '16px',
},
});
const iconComp = computed(() => {
if (props.icon.startsWith('http')) {
return () => h('img', { src: props.icon, class: 'm-icon__' });
}
return '';
});
const styles = computed(() => {
return {
fontSize: props.size.toString().endsWith('px')
? props.size
: `${props.size}px`,
};
});
</script>
<template>
<component :is="iconComp" v-if="iconComp" :style="styles" />
<VbenIcon v-else :icon="props.icon" :style="styles" class="m-icon__" />
</template>
<style lang="less" scoped>
.m-icon__ {
display: inline-flex;
align-items: center;
width: 1em;
height: 1em;
font-style: normal;
line-height: 0;
color: inherit;
text-align: center;
text-transform: none;
vertical-align: -0.125em;
text-rendering: optimizelegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>

View File

@ -1,225 +0,0 @@
<script setup lang="ts">
import type { ButtonType } from 'ant-design-vue/es/button';
import type { ActionItem, PopConfirm } from './types';
import { computed, type PropType, toRaw } from 'vue';
import { useAccess } from '@vben/access';
import { isBoolean, isFunction } from '@vben/utils';
import { Button, Dropdown, Menu, Popconfirm, Space } from 'ant-design-vue';
import { Icon } from '#/components/icon';
const props = defineProps({
actions: {
type: Array as PropType<ActionItem[]>,
default() {
return [];
},
},
dropDownActions: {
type: Array as PropType<ActionItem[]>,
default() {
return [];
},
},
divider: {
type: Boolean,
default: true,
},
});
const MenuItem = Menu.Item;
const { hasAccessByCodes } = useAccess();
function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
return isIfShow;
}
const getActions = computed(() => {
return (toRaw(props.actions) || [])
.filter((action) => {
return (
(hasAccessByCodes(action.auth || []) ||
(action.auth || []).length === 0) &&
isIfShow(action)
);
})
.map((action) => {
const { popConfirm } = action;
return {
// getPopupContainer: document.body,
type: 'link' as ButtonType,
...action,
...popConfirm,
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
enable: !!popConfirm,
};
});
});
const getDropdownList = computed((): any[] => {
return (toRaw(props.dropDownActions) || [])
.filter((action) => {
return (
(hasAccessByCodes(action.auth || []) ||
(action.auth || []).length === 0) &&
isIfShow(action)
);
})
.map((action, index) => {
const { label, popConfirm } = action;
return {
...action,
...popConfirm,
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
text: label,
divider:
index < props.dropDownActions.length - 1 ? props.divider : false,
};
});
});
const getPopConfirmProps = (attrs: PopConfirm) => {
const originAttrs: any = attrs;
delete originAttrs.icon;
if (attrs.confirm && isFunction(attrs.confirm)) {
originAttrs.onConfirm = attrs.confirm;
delete originAttrs.confirm;
}
if (attrs.cancel && isFunction(attrs.cancel)) {
originAttrs.onCancel = attrs.cancel;
delete originAttrs.cancel;
}
return originAttrs;
};
const getButtonProps = (action: ActionItem) => {
const res = {
type: action.type || 'primary',
...action,
};
delete res.icon;
return res;
};
const handleMenuClick = (e: any) => {
const action = getDropdownList.value[e.key];
if (action.onClick && isFunction(action.onClick)) {
action.onClick();
}
};
</script>
<template>
<div class="m-table-action">
<Space
:size="
getActions?.some((item: ActionItem) => item.type === 'link') ? 0 : 8
"
>
<template v-for="(action, index) in getActions" :key="index">
<Popconfirm
v-if="action.popConfirm"
v-bind="getPopConfirmProps(action.popConfirm)"
>
<template v-if="action.popConfirm.icon" #icon>
<Icon :icon="action.popConfirm.icon" />
</template>
<Button v-bind="getButtonProps(action)">
<template v-if="action.icon" #icon>
<Icon :icon="action.icon" />
</template>
{{ action.label }}
</Button>
</Popconfirm>
<Button v-else v-bind="getButtonProps(action)" @click="action.onClick">
<template v-if="action.icon" #icon>
<Icon :icon="action.icon" />
</template>
{{ action.label }}
</Button>
</template>
</Space>
<Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']">
<slot name="more">
<Button size="small" type="link">
<template #icon>
<Icon class="icon-more" icon="ant-design:more-outlined" />
</template>
</Button>
</slot>
<template #overlay>
<Menu @click="handleMenuClick">
<MenuItem v-for="(action, index) in getDropdownList" :key="index">
<template v-if="action.popConfirm">
<Popconfirm v-bind="getPopConfirmProps(action.popConfirm)">
<template v-if="action.popConfirm.icon" #icon>
<Icon :icon="action.popConfirm.icon" />
</template>
<div
:class="
action.disabled === true
? 'cursor-not-allowed text-gray-300'
: ''
"
>
<Icon v-if="action.icon" :icon="action.icon" />
<span class="ml-1">{{ action.text }}</span>
</div>
</Popconfirm>
</template>
<template v-else>
<div
:class="
action.disabled === true
? 'cursor-not-allowed text-gray-300'
: ''
"
>
<Icon v-if="action.icon" :icon="action.icon" />
{{ action.label }}
</div>
</template>
</MenuItem>
</Menu>
</template>
</Dropdown>
</div>
</template>
<style lang="less">
/** 修复 iconify 位置问题 **/
.m-table-action {
.ant-btn > .iconify + span,
.ant-btn > span + .iconify {
margin-inline-start: 8px;
}
.ant-btn > .iconify {
display: inline-flex;
align-items: center;
width: 1em;
height: 1em;
font-style: normal;
line-height: 0;
color: inherit;
text-align: center;
text-transform: none;
vertical-align: -0.125em;
text-rendering: optimizelegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
</style>

View File

@ -1,216 +0,0 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { z } from '@vben/common-ui';
import {
Button,
Col,
Image,
message as Message,
Row,
Spin,
Step,
Steps,
TabPane,
Tabs,
} from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
postUsersChangePassword,
postUsersMyProfile,
} from '#/api-client';
import { $t } from '#/locales';
defineOptions({
name: 'MyProfile',
});
const activeName = ref(0);
const loading = ref(false);
const [ProfileForm, profileFormApi] = useVbenForm({
//
collapsed: false,
//
commonConfig: {
//
componentProps: {
class: 'w-4/5',
},
},
//
handleSubmit: () => {},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.userName'),
},
fieldName: 'userName',
label: $t('abp.user.userName'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.userName'),
}),
disabled: true,
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.name'),
},
fieldName: 'name',
disabled: true,
label: $t('abp.user.name'),
},
// {
// component: 'VbenInput',
// componentProps: {
// placeholder: $t('common.pleaseInput') + $t('abp.user.surname'),
// },
// fieldName: 'surname',
// label: $t('abp.user.surname'),
// rules: z.string().min(1, {
// message: $t('common.pleaseInput') + $t('abp.user.surname'),
// }),
// },
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.email'),
},
fieldName: 'email',
disabled: true,
label: $t('abp.user.email'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.email'),
}),
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.phone'),
},
fieldName: 'phoneNumber',
disabled: true,
label: $t('abp.user.phone'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.phone'),
}),
},
],
// showCollapseButton: false,
// showDefaultActions: false,
wrapperClass: 'grid-cols-1',
resetButtonOptions: {
show: false,
},
submitButtonOptions: {
show: false,
},
});
const [ResetPasswordForm, resetPasswordApi] = useVbenForm({
//
collapsed: false,
//
commonConfig: {
//
componentProps: {
class: 'w-4/5',
},
},
//
handleSubmit: async () => {
//
const { valid } = await resetPasswordApi.validate();
if (!valid) return;
const formValues = await resetPasswordApi.getValues();
if (formValues.currentPassword === formValues.confirmPassword) {
Message.warn($t('abp.user.newPasswordAndCurrentPasswordNotAlike'));
return;
}
if (formValues.newPassword !== formValues.confirmPassword) {
Message.warn($t('abp.user.newPasswordAndConfirmPasswordNotMatch'));
return;
}
await postUsersChangePassword({ body: formValues });
Message.success($t('common.editSuccess'));
await resetPasswordApi.resetForm();
},
layout: 'horizontal',
schema: [
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.currentPassword'),
},
fieldName: 'currentPassword',
label: $t('abp.user.currentPassword'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.currentPassword'),
}),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.newPassword'),
},
fieldName: 'newPassword',
label: $t('abp.user.newPassword'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.newPassword'),
}),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.comfirmPassword'),
},
fieldName: 'confirmPassword',
label: $t('abp.user.comfirmPassword'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.comfirmPassword'),
}),
},
],
resetButtonOptions: {
show: false,
},
submitButtonOptions: {
content: '确认',
},
wrapperClass: 'grid-cols-1',
});
onMounted(async () => {
try {
loading.value = true;
const resp = await postUsersMyProfile();
await profileFormApi.setValues({ ...resp.data });
} finally {
loading.value = false;
}
});
</script>
<template>
<div>
<Spin :spinning="loading" tip="loading...">
<div class="bg-card px-8">
<Tabs
v-model:active-key="activeName"
tab-position="left"
@change="activeChange"
>
<TabPane :key="0" :tab="$t('abp.user.myProfile')">
<ProfileForm />
</TabPane>
<TabPane :key="1" :tab="$t('abp.user.changePassword')">
<ResetPasswordForm />
</TabPane>
</Tabs>
</div>
</Spin>
</div>
</template>

View File

@ -1,37 +0,0 @@
{
"companyName": "CompanyName",
"projectName": "ProjectName",
"projectEnglishName": "ProjectEnglishName",
"namespace": "Namespace",
"remark": "Remark",
"templateName": "TemplateName",
"model": "Model",
"property": "Property",
"enum": "Enum",
"isRequired": "IsRequired",
"maxLength": "MaxLength",
"minLength": "MinLength",
"decimalPrecision18": "Accuracy (18,6) of 18",
"decimalPrecision6": "Accuracy (18,6) of 6",
"pleaseSelectEntity": "Please select entity",
"pleaseSelectEnum": "Please select enum",
"detail": "Detail",
"copy": "Copy",
"addFolder": "AddFolder",
"addFile": "AddFile",
"desc": "Description",
"name": "Name",
"templateType": "TemplateType",
"supportTenant": "Support Multi-Tenant",
"project": "Project",
"template": "Template",
"preview": "Preview",
"download": "Download",
"description": "Please select the target and project to generate",
"autoGenerate": "Auto Generate Code",
"code": "Code",
"type": "Type",
"value": "Value",
"relational": "Relational",
"dataType": "DataType"
}

View File

@ -1,37 +0,0 @@
{
"companyName": "公司名称",
"projectName": "项目名称",
"projectEnglishName": "项目英文名称",
"namespace": "命名空间",
"remark": "备注",
"templateName": "模板名称",
"model": "模型",
"property": "属性",
"enum": "枚举",
"isRequired": "是否必填",
"maxLength": "最大长度",
"minLength": "最小长度",
"decimalPrecision18": "精度(18,6)中的18",
"decimalPrecision6": "精度(18,6)中的6",
"pleaseSelectEntity": "请选择实体",
"pleaseSelectEnum": "请选择枚举",
"detail": "详情",
"copy": "复制",
"addFolder": "新增文件夹",
"addFile": "新增文件",
"desc": "描述",
"name": "名称",
"templateType": "模板类型",
"supportTenant": "支持多租户",
"project": "项目",
"template": "模板",
"preview": "预览",
"download": "下载",
"description": "请选择要生成的目标和项目",
"autoGenerate": "自动生成代码",
"code": "编码",
"type": "类型",
"value": "值",
"relational": "关系",
"dataType": "数据类型"
}

View File

@ -1,114 +0,0 @@
import dayjs from 'dayjs';
import { $t } from '#/locales';
export const logQuerySchema: any = [
{
component: 'RangePicker',
fieldName: 'time',
label: $t('abp.log.loginTime'),
componentProps: {
'value-format': 'YYYY-MM-DD',
},
defaultValue: [
dayjs().subtract(0, 'day').format('YYYY-MM-DD'),
dayjs().format('YYYY-MM-DD'),
],
},
{
component: 'Input',
fieldName: 'userName',
label: $t('abp.log.userName'),
},
{
component: 'Input',
fieldName: 'correlationId',
label: 'CorrelationId',
},
];
export const logTableSchema: any = [
{ title: $t('common.seq'), type: 'seq', width: 50 },
{
field: 'applicationName',
title: $t('abp.log.applicationName'),
minWidth: '150',
},
{ field: 'identity', title: $t('abp.log.loginMode'), minWidth: '150' },
{ field: 'action', title: $t('abp.log.loginUrl'), minWidth: '150' },
{ field: 'userName', title: $t('abp.log.userName'), minWidth: '150' },
{ field: 'correlationId', title: 'CorrelationId', minWidth: '150' },
{ field: 'clientIpAddress', title: $t('abp.log.clientIp'), minWidth: '150' },
{
field: 'creationTime',
title: $t('common.createTime'),
minWidth: '150',
formatter: ({ cellValue }) => {
return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss');
},
},
];
export const auditLogQuerySchema: any = [
{
component: 'RangePicker',
fieldName: 'time',
label: $t('abp.log.executionTime'),
componentProps: {
'value-format': 'YYYY-MM-DD',
},
defaultValue: [
dayjs().subtract(0, 'day').format('YYYY-MM-DD'),
dayjs().format('YYYY-MM-DD'),
],
},
{
component: 'Input',
fieldName: 'userName',
label: $t('abp.log.userName'),
},
{
component: 'Input',
fieldName: 'correlationId',
label: 'CorrelationId',
},
{
component: 'Input',
fieldName: 'url',
label: 'Url',
},
];
export const auditLogTableSchema: any = [
{ title: $t('common.seq'), type: 'seq', width: 50 },
{ field: 'url', title: 'Url', minWidth: '150' },
{ field: 'tenantName', title: $t('abp.log.tenant'), minWidth: '150' },
{ field: 'userName', title: $t('abp.log.userName'), minWidth: '150' },
{
field: 'executionTime',
title: $t('abp.log.executionTime'),
minWidth: '150',
formatter: ({ cellValue }) => {
return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss');
},
},
{
field: 'executionDuration',
title: $t('abp.log.responseTime'),
minWidth: '150',
},
{
field: 'clientIpAddress',
title: $t('abp.log.clientIp'),
minWidth: '150',
},
{ field: 'correlationId', title: 'CorrelationId', minWidth: '150' },
{ field: 'exceptions', title: $t('abp.log.exception'), minWidth: '150' },
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 120,
},
];

View File

@ -1,55 +0,0 @@
<script setup lang="ts">
import { defineEmits, defineProps } from 'vue';
import { Button, Menu } from 'ant-design-vue';
defineProps({
x: Number,
y: Number,
options: Array,
});
const emit = defineEmits(['select', 'close']);
</script>
<template>
<div
:style="{ left: `${x}px`, top: `${y}px` }"
class="context-menu"
@click.stop
>
<Menu mode="vertical">
<Menu.Item
v-for="option in options"
:key="option.key"
@click.stop="$emit('select', option.key)"
>
<Button size="small" type="link"> {{ option.label }} </Button>
</Menu.Item>
</Menu>
</div>
</template>
<style scoped>
.context-menu {
position: fixed;
z-index: 1000;
background: white;
box-shadow: 2px 2px 5px rgb(0 0 0 / 10%);
}
ul {
padding: 0;
margin: 0;
list-style-type: none;
}
li {
padding: 8px 12px;
cursor: pointer;
}
li:hover {
background-color: #f0f0f0;
}
</style>

8
apps/web-ele/.env Normal file
View File

@ -0,0 +1,8 @@
# 应用标题
VITE_APP_TITLE=Abp Vben5 Ele
# 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=abp-vnext-pro-vben5-ele
# 对store进行加密的密钥在将store持久化到localStorage时会使用该密钥进行加密
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key

View File

@ -9,13 +9,13 @@ VITE_GLOB_API_URL=/api
# 是否开启 Nitro Mock服务true 为开启false 为关闭 # 是否开启 Nitro Mock服务true 为开启false 为关闭
VITE_NITRO_MOCK=true VITE_NITRO_MOCK=true
# 是否打开 devtoolstrue 为打开false 为关闭
VITE_DEVTOOLS=false
# vue-router 的模式 # vue-router 的模式
VITE_ROUTER_HISTORY=history VITE_ROUTER_HISTORY=history
# 是否打开 devtoolstrue 为打开false 为关闭
VITE_DEVTOOLS=false
# 是否注入全局loading # 是否注入全局loading
VITE_INJECT_APP_LOADING=true VITE_INJECT_APP_LOADING=true
@ -23,7 +23,7 @@ VITE_INJECT_APP_LOADING=true
VITE_REFRESH_ROLE = true VITE_REFRESH_ROLE = true
# 后端接口地址 # 后端接口地址
VITE_APP_API_ADDRESS=http://localhost:44315/ VITE_APP_API_ADDRESS=http://localhost:44315
# websocket地址 # websocket地址
VITE_WEBSOCKET_URL=http://localhost:44315/signalr/notification VITE_WEBSOCKET_URL=http://localhost:44315/signalr/notification

View File

@ -10,11 +10,15 @@ VITE_COMPRESS=none
VITE_PWA=false VITE_PWA=false
# vue-router 的模式 # vue-router 的模式
VITE_ROUTER_HISTORY=history VITE_ROUTER_HISTORY=hash
# 是否注入全局loading # 是否注入全局loading
VITE_INJECT_APP_LOADING=true VITE_INJECT_APP_LOADING=true
# vue-router 的模式
VITE_ROUTER_HISTORY=history
# 打包后是否生成dist.zip # 打包后是否生成dist.zip
VITE_ARCHIVER=true VITE_ARCHIVER=true
@ -22,7 +26,7 @@ VITE_ARCHIVER=true
VITE_REFRESH_ROLE = true VITE_REFRESH_ROLE = true
# 后端接口地址 # 后端接口地址
VITE_APP_API_ADDRESS=http://118.190.144.92:9110/ VITE_APP_API_ADDRESS=http://182.43.18.151:44317/
# websocket地址 # websocket地址
VITE_WEBSOCKET_URL=http://118.190.144.92:9110/signalr/notification VITE_WEBSOCKET_URL=http://182.43.18.151:44317/signalr/notification

View File

@ -21,7 +21,7 @@
(function () { (function () {
var hm = document.createElement('script'); var hm = document.createElement('script');
hm.src = hm.src =
'https://hm.baidu.com/hm.js?b38e689f40558f20a9a686d7f6f33edf'; 'https://hm.baidu.com/hm.js?97352b16ed2df8c3860cf5a1a65fb4dd';
var s = document.getElementsByTagName('script')[0]; var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s); s.parentNode.insertBefore(hm, s);
})(); })();

View File

@ -1,12 +1,12 @@
{ {
"name": "@vben/web-antd", "name": "@vben/web-ele",
"version": "5.5.3", "version": "5.5.6",
"homepage": "https://vben.pro", "homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git", "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "apps/web-antd" "directory": "apps/web-ele"
}, },
"license": "MIT", "license": "MIT",
"author": { "author": {
@ -27,6 +27,7 @@
"#/*": "./src/*" "#/*": "./src/*"
}, },
"dependencies": { "dependencies": {
"@iconify/json": "^2.2.282",
"@microsoft/signalr": "^8.0.7", "@microsoft/signalr": "^8.0.7",
"@vben/access": "workspace:*", "@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*", "@vben/common-ui": "workspace:*",
@ -43,11 +44,10 @@
"@vben/types": "workspace:*", "@vben/types": "workspace:*",
"@vben/utils": "workspace:*", "@vben/utils": "workspace:*",
"@vueuse/core": "catalog:", "@vueuse/core": "catalog:",
"ant-design-vue": "catalog:",
"axios": "^1.7.7", "axios": "^1.7.7",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"codemirror-editor-vue3": "^2.8.0",
"dayjs": "catalog:", "dayjs": "catalog:",
"element-plus": "catalog:",
"pinia": "catalog:", "pinia": "catalog:",
"vue": "catalog:", "vue": "catalog:",
"vue-request": "^2.0.4", "vue-request": "^2.0.4",
@ -56,6 +56,7 @@
}, },
"devDependencies": { "devDependencies": {
"@hey-api/client-axios": "^0.2.10", "@hey-api/client-axios": "^0.2.10",
"@hey-api/openapi-ts": "^0.55.3" "@hey-api/openapi-ts": "^0.55.3",
"unplugin-element-plus": "catalog:"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,338 @@
/**
* 使 adapter/form 使便使
* vben-formvben-modalvben-drawer 使,
*/
import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import {
defineAsyncComponent,
defineComponent,
getCurrentInstance,
h,
ref,
} from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { ElNotification } from 'element-plus';
const ElButton = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/button/index'),
import('element-plus/es/components/button/style/css'),
]).then(([res]) => res.ElButton),
);
const ElCheckbox = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/checkbox/index'),
import('element-plus/es/components/checkbox/style/css'),
]).then(([res]) => res.ElCheckbox),
);
const ElCheckboxButton = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/checkbox/index'),
import('element-plus/es/components/checkbox-button/style/css'),
]).then(([res]) => res.ElCheckboxButton),
);
const ElCheckboxGroup = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/checkbox/index'),
import('element-plus/es/components/checkbox-group/style/css'),
]).then(([res]) => res.ElCheckboxGroup),
);
const ElDatePicker = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/date-picker/index'),
import('element-plus/es/components/date-picker/style/css'),
]).then(([res]) => res.ElDatePicker),
);
const ElDivider = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/divider/index'),
import('element-plus/es/components/divider/style/css'),
]).then(([res]) => res.ElDivider),
);
const ElInput = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/input/index'),
import('element-plus/es/components/input/style/css'),
]).then(([res]) => res.ElInput),
);
const ElInputNumber = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/input-number/index'),
import('element-plus/es/components/input-number/style/css'),
]).then(([res]) => res.ElInputNumber),
);
const ElRadio = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/radio/index'),
import('element-plus/es/components/radio/style/css'),
]).then(([res]) => res.ElRadio),
);
const ElRadioButton = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/radio/index'),
import('element-plus/es/components/radio-button/style/css'),
]).then(([res]) => res.ElRadioButton),
);
const ElRadioGroup = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/radio/index'),
import('element-plus/es/components/radio-group/style/css'),
]).then(([res]) => res.ElRadioGroup),
);
const ElSelectV2 = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/select-v2/index'),
import('element-plus/es/components/select-v2/style/css'),
]).then(([res]) => res.ElSelectV2),
);
const ElSpace = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/space/index'),
import('element-plus/es/components/space/style/css'),
]).then(([res]) => res.ElSpace),
);
const ElSwitch = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/switch/index'),
import('element-plus/es/components/switch/style/css'),
]).then(([res]) => res.ElSwitch),
);
const ElTimePicker = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/time-picker/index'),
import('element-plus/es/components/time-picker/style/css'),
]).then(([res]) => res.ElTimePicker),
);
const ElTreeSelect = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/tree-select/index'),
import('element-plus/es/components/tree-select/style/css'),
]).then(([res]) => res.ElTreeSelect),
);
const ElUpload = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/upload/index'),
import('element-plus/es/components/upload/style/css'),
]).then(([res]) => res.ElUpload),
);
const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',
componentProps: Recordable<any> = {},
) => {
return defineComponent({
name: component.name,
inheritAttrs: false,
setup: (props: any, { attrs, expose, slots }) => {
const placeholder =
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
const publicApi: Recordable<any> = {};
expose(publicApi);
const instance = getCurrentInstance();
instance?.proxy?.$nextTick(() => {
for (const key in innerRef.value) {
if (typeof innerRef.value[key] === 'function') {
publicApi[key] = innerRef.value[key];
}
}
});
return () =>
h(
component,
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
slots,
);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'Divider'
| 'IconPicker'
| 'Input'
| 'InputNumber'
| 'RadioGroup'
| 'Select'
| 'Space'
| 'Switch'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiSelect',
},
'select',
{
component: ElSelectV2,
loadingSlot: 'loading',
visibleEvent: 'onVisibleChange',
},
),
ApiTreeSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiTreeSelect',
},
'select',
{
component: ElTreeSelect,
props: { label: 'label', children: 'children' },
nodeKey: 'value',
loadingSlot: 'loading',
optionsPropName: 'data',
visibleEvent: 'onVisibleChange',
},
),
Checkbox: ElCheckbox,
CheckboxGroup: (props, { attrs, slots }) => {
let defaultSlot;
if (Reflect.has(slots, 'default')) {
defaultSlot = slots.default;
} else {
const { options, isButton } = attrs;
if (Array.isArray(options)) {
defaultSlot = () =>
options.map((option) =>
h(isButton ? ElCheckboxButton : ElCheckbox, option),
);
}
}
return h(
ElCheckboxGroup,
{ ...props, ...attrs },
{ ...slots, default: defaultSlot },
);
},
// 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => {
return h(ElButton, { ...props, attrs, type: 'info' }, slots);
},
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {
return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
},
Divider: ElDivider,
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
iconSlot: 'append',
modelValueProp: 'model-value',
inputComponent: ElInput,
}),
Input: withDefaultPlaceholder(ElInput, 'input'),
InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
RadioGroup: (props, { attrs, slots }) => {
let defaultSlot;
if (Reflect.has(slots, 'default')) {
defaultSlot = slots.default;
} else {
const { options } = attrs;
if (Array.isArray(options)) {
defaultSlot = () =>
options.map((option) =>
h(attrs.isButton ? ElRadioButton : ElRadio, option),
);
}
}
return h(
ElRadioGroup,
{ ...props, ...attrs },
{ ...slots, default: defaultSlot },
);
},
Select: (props, { attrs, slots }) => {
return h(ElSelectV2, { ...props, attrs }, slots);
},
Space: ElSpace,
Switch: ElSwitch,
TimePicker: (props, { attrs, slots }) => {
const { name, id, isRange } = props;
const extraProps: Recordable<any> = {};
if (isRange) {
if (name && !Array.isArray(name)) {
extraProps.name = [name, `${name}_end`];
}
if (id && !Array.isArray(id)) {
extraProps.id = [id, `${id}_end`];
}
}
return h(
ElTimePicker,
{
...props,
...attrs,
...extraProps,
},
slots,
);
},
DatePicker: (props, { attrs, slots }) => {
const { name, id, type } = props;
const extraProps: Recordable<any> = {};
if (type && type.includes('range')) {
if (name && !Array.isArray(name)) {
extraProps.name = [name, `${name}_end`];
}
if (id && !Array.isArray(id)) {
extraProps.id = [id, `${id}_end`];
}
}
return h(
ElDatePicker,
{
...props,
...attrs,
...extraProps,
},
slots,
);
},
TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
Upload: ElUpload,
};
// 将组件注册到全局共享状态中
globalShareState.setComponents(components);
// 定义全局共享状态中的消息提示
globalShareState.defineMessage({
// 复制成功消息提示
copyPreferencesSuccess: (title, content) => {
ElNotification({
title,
message: content,
position: 'bottom-right',
duration: 0,
type: 'success',
});
},
});
}
export { initComponentAdapter };

View File

@ -10,26 +10,18 @@ import { $t } from '@vben/locales';
setupVbenForm<ComponentType>({ setupVbenForm<ComponentType>({
config: { config: {
// ant design vue组件库默认都是 v-model:value
baseModelPropName: 'value',
// 一些组件是 v-model:checked 或者 v-model:fileList
modelPropNameMap: { modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
Switch: 'checked',
Upload: 'fileList', Upload: 'fileList',
CheckboxGroup: 'model-value',
}, },
}, },
defineRules: { defineRules: {
// 输入项目必填国际化适配
required: (value, _params, ctx) => { required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) { if (value === undefined || value === null || value.length === 0) {
return $t('ui.formRules.required', [ctx.label]); return $t('ui.formRules.required', [ctx.label]);
} }
return true; return true;
}, },
// 选择项目必填国际化适配
selectRequired: (value, _params, ctx) => { selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) { if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]); return $t('ui.formRules.selectRequired', [ctx.label]);

View File

@ -2,7 +2,7 @@ import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { Button, Image } from 'ant-design-vue'; import { ElButton, ElImage } from 'element-plus';
import { useVbenForm } from './form'; import { useVbenForm } from './form';
@ -20,7 +20,11 @@ setupVbenVxeTable({
// 全局禁用vxe-table的表单配置使用formOptions // 全局禁用vxe-table的表单配置使用formOptions
enabled: false, enabled: false,
}, },
sortConfig: {
remote: true,
},
proxyConfig: { proxyConfig: {
sort: true, // 启用排序请求代理
autoLoad: true, autoLoad: true,
response: { response: {
result: 'items', result: 'items',
@ -32,7 +36,8 @@ setupVbenVxeTable({
}, },
round: true, round: true,
showOverflow: true, showOverflow: true,
size: 'small', // medium / small / mini stripe: true, // 斑马线
size: 'small',
}, },
}); });
@ -40,7 +45,8 @@ setupVbenVxeTable({
vxeUI.renderer.add('CellImage', { vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) { renderTableDefault(_renderOpts, params) {
const { column, row } = params; const { column, row } = params;
return h(Image, { src: row[column.field] }); const src = row[column.field];
return h(ElImage, { src, previewSrcList: [src] });
}, },
}); });
@ -49,8 +55,8 @@ setupVbenVxeTable({
renderTableDefault(renderOpts) { renderTableDefault(renderOpts) {
const { props } = renderOpts; const { props } = renderOpts;
return h( return h(
Button, ElButton,
{ size: 'small', type: 'link' }, { size: 'small', link: true },
{ default: () => props?.text }, { default: () => props?.text },
); );
}, },

View File

@ -2,6 +2,6 @@ import { defineConfig } from '@hey-api/openapi-ts';
export default defineConfig({ export default defineConfig({
client: '@hey-api/client-axios', client: '@hey-api/client-axios',
input: 'http://localhost:44315/swagger/IOT/swagger.json', input: 'http://localhost:44315/swagger/AbpPro/swagger.json',
output: 'src/api-client', output: 'src/api-client',
}); });

View File

@ -0,0 +1,146 @@
import { useAccessStore, useUserStore } from '@vben/stores';
import { ElMessage as Message } from 'element-plus';
import { postApiAppAccountRefreshToken } from '#/api-client';
import { $t } from '#/locales';
import { elementLocale } from '#/locales/index';
import { useAuthStore } from '#/store';
import { client } from '../api-client/services.gen';
client.setConfig({
baseURL: import.meta.env.DEV
? '/proxy/'
: import.meta.env.VITE_APP_API_ADDRESS,
timeout: 1000 * 60,
responseType: 'json',
throwOnError: true,
});
// 是否正在刷新token
let isRefreshing = false;
// 刷新token队列
let refreshTokenQueue: ((token: string) => void)[] = [];
client.instance.interceptors.request.use((request) => {
// 全局拦截请求发送前提交的参数
const userStore = useUserStore();
const accessStore = useAccessStore();
const token = accessStore.getAccessToken();
// 设置请求头
if (request.headers) {
request.headers.__tenant = userStore.tenant?.tenantId;
// todo vben5 没有提供统一获取当前语言的方式
request.headers['accept-language'] = elementLocale.value.name;
}
// 如果token过期则跳转到登录页面
if (
request.url !== undefined &&
request.url.includes('/api/app/account/login')
) {
return request;
}
// 设置请求头
if (request.headers) {
request.headers.Authorization = `Bearer ${token}`;
}
return request;
});
client.instance.interceptors.response.use(
(response) => {
return Promise.resolve(response);
},
async (error) => {
let message = error.message;
if (message === 'Network Error') {
message = $t('common.mesage500');
} else if (message.includes('timeout')) {
message = $t('common.timeOut');
} else
switch (error.status) {
case 400: {
message = error.response.data.error?.validationErrors[0].message;
break;
}
case 401: {
message = $t('common.mesage401');
const { config } = error;
const originalRequest = config;
if (isRefreshing) {
return new Promise((resolve) => {
refreshTokenQueue.push((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(client.request(originalRequest));
});
});
} else {
isRefreshing = true;
try {
const newToken = await refreshTokenAsync();
// 处理队列中的请求
refreshTokenQueue.forEach((callback) => callback(newToken));
// 清空队列
refreshTokenQueue = [];
return client.request(originalRequest);
} catch (refreshError) {
// 如果刷新 token 失败,处理错误(如强制登出或跳转登录页面)
message = $t('common.mesage401');
refreshTokenQueue = [];
const authStore = useAuthStore();
authStore.logout();
console.error(refreshError);
} finally {
isRefreshing = false;
}
}
break;
}
case 403: {
message = $t('common.mesage403');
break;
}
case 500: {
message = error.response.data.error?.message;
break;
}
default: {
if (message.includes('Request failed with status code')) {
message = $t('common.mesage500');
}
}
}
Message.error(message);
throw error;
},
);
async function refreshTokenAsync(): Promise<string> {
try {
const userStore = useUserStore();
const accessStore = useAccessStore();
const refreshToken = accessStore.getRefreshToken();
if (!refreshToken) return '';
const res = await postApiAppAccountRefreshToken({
body: {
userId: userStore.userInfo?.id,
refreshToken,
},
});
if (res?.data?.success) {
accessStore.setAccessToken(res.data.token as string);
accessStore.setRefreshToken(res.data.refreshToken as string);
return res.data.token as string;
} else {
throw new Error('get refreshToken error');
}
} catch {
throw new Error($t('common.mesage401'));
}
}
export default client;

File diff suppressed because one or more lines are too long

View File

@ -13,7 +13,7 @@ import {
} from '@vben/request'; } from '@vben/request';
import { useAccessStore } from '@vben/stores'; import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue'; import { ElMessage } from 'element-plus';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
@ -99,7 +99,7 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
const responseData = error?.response?.data ?? {}; const responseData = error?.response?.data ?? {};
const errorMessage = responseData?.error ?? responseData?.message ?? ''; const errorMessage = responseData?.error ?? responseData?.message ?? '';
// 如果没有错误信息,则会根据状态码进行提示 // 如果没有错误信息,则会根据状态码进行提示
message.error(errorMessage || msg); ElMessage.error(errorMessage || msg);
}), }),
); );

17
apps/web-ele/src/app.vue Normal file
View File

@ -0,0 +1,17 @@
<script lang="ts" setup>
import { useElementPlusDesignTokens } from '@vben/hooks';
import { ElConfigProvider } from 'element-plus';
import { elementLocale } from '#/locales';
defineOptions({ name: 'App' });
useElementPlusDesignTokens();
</script>
<template>
<ElConfigProvider :locale="elementLocale">
<RouterView />
</ElConfigProvider>
</template>

View File

@ -1,15 +1,14 @@
import { createApp, watchEffect } from 'vue'; import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access'; import { registerAccessDirective } from '@vben/access';
import { initTippy } from '@vben/common-ui'; import { registerLoadingDirective } from '@vben/common-ui';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores'; import { initStores } from '@vben/stores';
import '@vben/styles'; import '@vben/styles';
import '@vben/styles/antd'; import '@vben/styles/ele';
import { useTitle } from '@vueuse/core'; import { useTitle } from '@vueuse/core';
// https://github.com/rennzhang/codemirror-editor-vue3 import { ElLoading } from 'element-plus';
import { InstallCodeMirror } from 'codemirror-editor-vue3';
import JsonViewer from 'vue3-json-viewer'; import JsonViewer from 'vue3-json-viewer';
import { $t, setupI18n } from '#/locales'; import { $t, setupI18n } from '#/locales';
@ -18,23 +17,32 @@ import { initComponentAdapter } from './adapter/component';
import App from './app.vue'; import App from './app.vue';
import { router } from './router'; import { router } from './router';
// 加载本地图标
// import '#/hooks/useLoadIcon';
import 'vue3-json-viewer/dist/index.css'; import 'vue3-json-viewer/dist/index.css';
async function bootstrap(namespace: string) { async function bootstrap(namespace: string) {
// 初始化组件适配器 // 初始化组件适配器
await initComponentAdapter(); await initComponentAdapter();
// // 设置弹窗的默认配置 // // 设置弹窗的默认配置
// setDefaultModalProps({ // setDefaultModalProps({
// fullscreenButton: false, // fullscreenButton: false,
// }); // });
// // 设置抽屉的默认配置 // // 设置抽屉的默认配置
// setDefaultDrawerProps({ // setDefaultDrawerProps({
// zIndex: 1020, // zIndex: 2000,
// }); // });
const app = createApp(App); const app = createApp(App);
// 注册Element Plus提供的v-loading指令
app.directive('loading', ElLoading.directive);
// 注册Vben提供的v-loading和v-spinning指令
registerLoadingDirective(app, {
loading: false, // Vben提供的v-loading指令和Element Plus提供的v-loading指令二选一即可此处false表示不注册Vben提供的v-loading指令
spinning: 'spinning',
});
// 国际化 i18n 配置 // 国际化 i18n 配置
await setupI18n(app); await setupI18n(app);
@ -45,13 +53,18 @@ async function bootstrap(namespace: string) {
registerAccessDirective(app); registerAccessDirective(app);
// 初始化 tippy // 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app); initTippy(app);
// 配置路由及路由守卫 // 配置路由及路由守卫
app.use(router); app.use(router);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
// 配置 json-viewer // 配置 json-viewer
app.use(JsonViewer); app.use(JsonViewer);
// 动态更新标题 // 动态更新标题
watchEffect(() => { watchEffect(() => {
if (preferences.app.dynamicTitle) { if (preferences.app.dynamicTitle) {
@ -61,7 +74,7 @@ async function bootstrap(namespace: string) {
useTitle(pageTitle); useTitle(pageTitle);
} }
}); });
app.use(InstallCodeMirror);
app.mount('#app'); app.mount('#app');
} }

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import { IconifyIcon as VbenIcon } from '@vben/icons';
import { ElIcon } from 'element-plus';
const props = defineProps({
icon: {
type: String,
default: '',
},
});
</script>
<template>
<ElIcon>
<VbenIcon :icon="props.icon" />
</ElIcon>
</template>

View File

@ -0,0 +1,201 @@
<script setup lang="ts">
import type { ActionItem, PopConfirm } from './types';
import { computed, h, type PropType, reactive, toRaw, type VNode } from 'vue';
import { useAccess } from '@vben/access';
import { isBoolean, isFunction } from '@vben/utils';
import {
ElButton,
ElDropdown,
ElDropdownItem,
ElDropdownMenu,
ElPopconfirm,
ElSpace,
} from 'element-plus';
import { Icon } from '#/components/icon';
const props = defineProps({
actions: {
type: Array as PropType<ActionItem[]>,
default() {
return [];
},
},
dropDownActions: {
type: Array as PropType<ActionItem[]>,
default() {
return [];
},
},
divider: {
type: Boolean,
default: true,
},
});
const renderIcon = (icon: string | VNode) => {
if (typeof icon === 'string') {
return h(Icon, { icon });
}
return h(icon);
};
const { hasAccessByCodes } = useAccess();
const popconfirmRef = reactive<any>({});
function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
return isIfShow;
}
const getActions = computed(() => {
return (toRaw(props.actions) || [])
.filter((action) => {
return (
(hasAccessByCodes(action.auth || []) ||
(action.auth || []).length === 0) &&
isIfShow(action)
);
})
.map((action) => {
const { popConfirm } = action;
const icon = action.icon ? renderIcon(action.icon) : undefined;
return {
// getPopupContainer: document.body,
...action,
...popConfirm,
icon,
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
enable: !!popConfirm,
};
});
});
const getDropdownList = computed((): any[] => {
return (toRaw(props.dropDownActions) || [])
.filter((action) => {
return (
(hasAccessByCodes(action.auth || []) ||
(action.auth || []).length === 0) &&
isIfShow(action)
);
})
.map((action, index) => {
const { label, popConfirm } = action;
return {
...action,
...popConfirm,
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
text: label,
divider:
index < props.dropDownActions.length - 1 ? props.divider : false,
};
});
});
const getPopConfirmProps = (attrs: PopConfirm) => {
const originAttrs: any = attrs;
delete originAttrs.icon;
if (attrs.confirm && isFunction(attrs.confirm)) {
originAttrs.onConfirm = attrs.confirm;
delete originAttrs.confirm;
}
if (attrs.cancel && isFunction(attrs.cancel)) {
originAttrs.onCancel = attrs.cancel;
delete originAttrs.cancel;
}
return originAttrs;
};
const getButtonProps = (action: ActionItem): any => {
const res = {
...action,
type: action.type || 'primary',
};
return res;
};
const handleCommand = (index: number) => {
const action = getDropdownList.value[index];
if (action.onClick && isFunction(action.onClick)) {
action.onClick();
} else {
const currentPopconfirmRef = popconfirmRef[index.toString()];
currentPopconfirmRef?.click();
}
};
const handleRef = (e: any, index: number) => {
popconfirmRef[index.toString()] = e;
};
</script>
<template>
<div class="m-table-action">
<ElSpace :size="2">
<template v-for="(action, index) in getActions" :key="index">
<ElPopconfirm
v-if="action.popConfirm"
v-bind="getPopConfirmProps(action.popConfirm)"
>
<template #reference>
<ElButton v-bind="getButtonProps(action)">
{{ action.label }}
</ElButton>
</template>
</ElPopconfirm>
<ElButton
v-else
v-bind="getButtonProps(action)"
@click="action.onClick"
>
{{ action.label }}
</ElButton>
</template>
</ElSpace>
<ElDropdown
v-if="getDropdownList.length > 0"
trigger="hover"
@command="handleCommand"
>
<template #default>
<div>
<slot name="more">
<ElButton link size="small" style="margin-left: 6px" type="primary">
<Icon class="icon-more" icon="ep:more" />
</ElButton>
</slot>
<template v-for="(action, index) in getDropdownList" :key="index">
<ElPopconfirm
v-if="action.popConfirm"
v-bind="getPopConfirmProps(action.popConfirm)"
>
<template #reference>
<span :ref="(e) => handleRef(e, index)"></span>
</template>
</ElPopconfirm>
</template>
</div>
</template>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem
v-for="(action, index) in getDropdownList"
:key="index"
:command="index"
>
<Icon v-if="action.icon" :icon="action.icon" />
{{ action.label }}
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</div>
</template>

View File

@ -1,5 +1,6 @@
import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes'; import type { VNode } from 'vue';
import { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip';
import { TooltipProps } from 'element-plus';
export interface PopConfirm { export interface PopConfirm {
title: string; title: string;
@ -8,13 +9,15 @@ export interface PopConfirm {
confirm: Fn; confirm: Fn;
cancel?: Fn; cancel?: Fn;
icon?: string; icon?: string;
disabled?: boolean;
} }
export interface ActionItem extends ButtonProps { export interface ActionItem {
type?: 'danger' | 'info' | 'primary' | 'success' | 'warning';
link?: boolean;
size?: 'large' | 'medium' | 'mini' | 'small';
onClick?: Fn; onClick?: Fn;
label?: string; label?: string;
color?: 'error' | 'success' | 'warning'; color?: 'error' | 'success' | 'warning';
icon?: string; icon?: string | VNode;
popConfirm?: PopConfirm; popConfirm?: PopConfirm;
disabled?: boolean; disabled?: boolean;
divider?: boolean; divider?: boolean;

View File

@ -0,0 +1,23 @@
import { addCollection } from '@vben/icons';
import AntDesignIcons from '@iconify/json/json/ant-design.json';
import CarbonIcons from '@iconify/json/json/carbon.json';
import EpIcons from '@iconify/json/json/ep.json';
import IcIcons from '@iconify/json/json/ic.json';
import LogosIcons from '@iconify/json/json/logos.json';
import LucideIcons from '@iconify/json/json/lucide.json';
import MdiIcons from '@iconify/json/json/mdi.json';
import OuiIcons from '@iconify/json/json/oui.json';
import PhosphorIcons from '@iconify/json/json/ph.json';
import UnIcons from '@iconify/json/json/uil.json';
addCollection(AntDesignIcons);
addCollection(LucideIcons);
addCollection(CarbonIcons);
addCollection(IcIcons as any);
addCollection(LogosIcons as any);
addCollection(PhosphorIcons as any);
addCollection(UnIcons);
addCollection(OuiIcons);
addCollection(MdiIcons);
addCollection(EpIcons);

View File

@ -2,7 +2,7 @@ import { useEventbus } from '@vben/hooks';
import { useUserStore } from '@vben/stores'; import { useUserStore } from '@vben/stores';
import * as signalR from '@microsoft/signalr'; import * as signalR from '@microsoft/signalr';
import { notification } from 'ant-design-vue'; import { ElNotification as notification } from 'element-plus';
const eventbus = useEventbus(); const eventbus = useEventbus();
let connection: signalR.HubConnection; let connection: signalR.HubConnection;
@ -12,6 +12,11 @@ export function useSignalR() {
*/ */
async function startConnect() { async function startConnect() {
try { try {
const userStore = useUserStore();
if (userStore.checkUserLoginExpire()) {
console.debug('未检测到用户信息,登录之后才会链接SignalR.');
return;
}
connectionsignalR(); connectionsignalR();
await connection.start(); await connection.start();
} catch (error) { } catch (error) {
@ -24,10 +29,7 @@ export function useSignalR() {
* SignalR连接 * SignalR连接
*/ */
function closeConnect(): void { function closeConnect(): void {
8; connection?.stop();
if (connection) {
connection.stop();
}
} }
async function connectionsignalR() { async function connectionsignalR() {
@ -75,21 +77,24 @@ export function useSignalR() {
// 发布事件 // 发布事件
eventbus.publish('ReceiveTextMessageHandlerAsync', message); eventbus.publish('ReceiveTextMessageHandlerAsync', message);
if (message.messageLevel === 10) { if (message.messageLevel === 10) {
notification.warn({ notification({
description: message.content, message: message.content,
message: message.title, title: message.title,
type: 'warning',
}); });
} }
if (message.messageLevel === 20) { if (message.messageLevel === 20) {
notification.info({ notification.info({
message: message.title, message: message.content,
description: message.content, title: message.title,
type: 'info',
}); });
} }
if (message.messageLevel === 30) { if (message.messageLevel === 30) {
notification.error({ notification.error({
message: message.title, message: message.content,
description: message.content, title: message.title,
type: 'error',
}); });
} }
} }
@ -100,23 +105,26 @@ export function useSignalR() {
*/ */
function ReceiveBroadCastMessageHandlerAsync(message: any) { function ReceiveBroadCastMessageHandlerAsync(message: any) {
// 发布事件 // 发布事件
eventbus.publish('ReceiveTextMessageHandlerAsync', message); eventbus.publish('ReceiveBroadCastMessageHandlerAsync', message);
if (message.messageLevel === 10) { if (message.messageLevel === 10) {
notification.warn({ notification({
message: message.title, message: message.content,
description: message.content, title: message.title,
type: 'warning',
}); });
} }
if (message.messageLevel === 20) { if (message.messageLevel === 20) {
notification.info({ notification.info({
message: message.title, message: message.content,
description: message.content, title: message.title,
type: 'info',
}); });
} }
if (message.messageLevel === 30) { if (message.messageLevel === 30) {
notification.error({ notification.error({
message: message.title, message: message.content,
description: message.content, title: message.title,
type: 'error',
}); });
} }
} }

View File

@ -5,15 +5,14 @@ import type { VxeGridProps } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { useUserStore } from '@vben/stores'; import { useUserStore } from '@vben/stores';
import { Button, message as Message, Modal, Space, Tag } from 'ant-design-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { ElMessage as Message } from 'element-plus';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { import {
postNotificationNotificationPage, postNotificationNotificationPage,
postNotificationRead, postNotificationRead,
} from '#/api-client'; } from '#/api-client';
import { $t } from '#/locales';
defineOptions({ defineOptions({
name: 'AbpNotifyItem', name: 'AbpNotifyItem',
@ -34,29 +33,29 @@ const formOptions: VbenFormProps = {
{ {
component: 'Input', component: 'Input',
fieldName: 'title', fieldName: 'title',
label: $t('abp.message.title'), label: '标题',
}, },
{ {
component: 'Input', component: 'Input',
fieldName: 'content', fieldName: 'content',
label: $t('abp.message.content'), label: '内容',
}, },
{ {
component: 'Select', component: 'Select',
fieldName: 'messageLevel', fieldName: 'messageLevel',
label: $t('abp.message.level'), label: '级别',
componentProps: { componentProps: {
options: [ options: [
{ {
label: $t('common.warning'), label: '警告',
value: 10, value: 10,
}, },
{ {
label: $t('common.info'), label: '正常',
value: 20, value: 20,
}, },
{ {
label: $t('common.error'), label: '错误',
value: 30, value: 30,
}, },
], ],
@ -66,15 +65,15 @@ const formOptions: VbenFormProps = {
{ {
component: 'Select', component: 'Select',
fieldName: 'read', fieldName: 'read',
label: $t('abp.message.isRead'), label: '是否已读',
componentProps: { componentProps: {
options: [ options: [
{ {
label: $t('common.yes'), label: '是',
value: true, value: true,
}, },
{ {
label: $t('common.no'), label: '否',
value: false, value: false,
}, },
], ],
@ -87,42 +86,34 @@ const userStore = useUserStore();
const gridOptions: VxeGridProps<any> = { const gridOptions: VxeGridProps<any> = {
checkboxConfig: {}, checkboxConfig: {},
columns: [ columns: [
{ title: $t('common.seq'), type: 'seq', width: 50 }, { title: '序号', type: 'seq', width: 50 },
{ field: 'title', title: $t('abp.message.title'), minWidth: '150' }, { field: 'title', title: '标题', minWidth: '150' },
{ field: 'content', title: $t('abp.message.content'), minWidth: '150' }, { field: 'content', title: '内容', minWidth: '150' },
// { field: 'messageTypeName', title: '', minWidth: '150' }, // { field: 'messageTypeName', title: '', minWidth: '150' },
{ {
field: 'messageLevelName', field: 'messageLevelName',
title: $t('abp.message.level'), title: '级别',
minWidth: '150', minWidth: '150',
slots: { default: 'messageLevel' }, slots: { default: 'messageLevel' },
}, },
{ { field: 'senderUserName', title: '发送人', minWidth: '150' },
field: 'senderUserName', { field: 'receiveUserName', title: '接收人', minWidth: '150' },
title: $t('abp.message.sender'),
minWidth: '150',
},
{
field: 'receiveUserName',
title: $t('abp.message.receiver'),
minWidth: '150',
},
{ {
field: 'read', field: 'read',
title: $t('abp.message.isRead'), title: '是否已读',
minWidth: '150', minWidth: '150',
slots: { default: 'read' }, slots: { default: 'read' },
}, },
{ {
field: 'creationTime', field: 'creationTime',
title: $t('common.createTime'), title: '创建时间',
minWidth: '150', minWidth: '150',
formatter: ({ cellValue }) => { formatter: ({ cellValue }) => {
return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss'); return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss');
}, },
}, },
{ {
title: $t('common.action'), title: '操作',
field: 'action', field: 'action',
fixed: 'right', fixed: 'right',
minWidth: '150', minWidth: '150',
@ -138,7 +129,6 @@ const gridOptions: VxeGridProps<any> = {
customConfig: { customConfig: {
storage: true, storage: true,
}, },
proxyConfig: { proxyConfig: {
ajax: { ajax: {
query: async ({ page }, formValues) => { query: async ({ page }, formValues) => {
@ -158,24 +148,19 @@ const gridOptions: VxeGridProps<any> = {
const [Grid, gridApi] = useVbenVxeGrid({ formOptions, gridOptions }); const [Grid, gridApi] = useVbenVxeGrid({ formOptions, gridOptions });
const onRead = (row: any) => { const onRead = async (row: any) => {
// if (row.read) { if (row.read) {
// Message.info(','); Message.info('该消息已读,不需要重复设置');
// return; return;
// } }
Modal.confirm({
title: $t('abp.message.confirmRead'),
onOk: async () => {
await postNotificationRead({ body: { id: row.id } }); await postNotificationRead({ body: { id: row.id } });
gridApi.reload(); gridApi.reload();
Message.success($t('common.success')); Message.success('设置成功');
},
});
}; };
</script> </script>
<template> <template>
<Page auto-content-height> <Page auto-content-height title="消息列表">
<Grid> <Grid>
<template #messageLevel="{ row }"> <template #messageLevel="{ row }">
<Tag v-if="row.messageLevel === 10" color="yellow"> <Tag v-if="row.messageLevel === 10" color="yellow">
@ -189,14 +174,14 @@ const onRead = (row: any) => {
</Tag> </Tag>
</template> </template>
<template #read="{ row }"> <template #read="{ row }">
<Tag v-if="row.read" color="green">{{ $t('abp.message.read') }} </Tag> <Tag v-if="row.read" color="green"> 已读 </Tag>
<Tag v-else color="red"> {{ $t('abp.message.unread') }} </Tag> <Tag v-else color="red"> 未读 </Tag>
</template> </template>
<template #action="{ row }"> <template #action="{ row }">
<Space> <Space>
<Button size="small" type="primary" @click="onRead(row)"> <Button size="small" type="primary" @click="onRead(row)">
{{ $t('abp.message.setRead') }} 设置已读
</Button> </Button>
</Space> </Space>
</template> </template>

View File

@ -2,10 +2,8 @@
import type { NotificationItem } from '@vben/layouts'; import type { NotificationItem } from '@vben/layouts';
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui'; import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui';
import { LOGIN_PATH } from '@vben/constants';
import { useEventbus, useRefresh, useWatermark } from '@vben/hooks'; import { useEventbus, useRefresh, useWatermark } from '@vben/hooks';
import { import {
BasicLayout, BasicLayout,
@ -16,24 +14,39 @@ import {
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores'; import { useAccessStore, useUserStore } from '@vben/stores';
import { message as Message } from 'ant-design-vue/es/components';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { ElMessage as Message } from 'element-plus';
import { import {
postNotificationBatchRead, postNotificationBatchRead,
postNotificationNotificationPage, postNotificationNotificationPage,
postNotificationRead, postNotificationRead,
postUsersNeedChangePassword,
} from '#/api-client'; } from '#/api-client';
import { useSignalR } from '#/hooks/useSignalR'; import { useSignalR } from '#/hooks/useSignalR';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue'; import LoginForm from '#/views/_core/authentication/login.vue';
import ChangePassword from './change-password.vue';
import MyProfile from './my-profile.vue'; import MyProfile from './my-profile.vue';
import NotifyItem from './NotifyItem.vue'; import NotifyItem from './NotifyItem.vue';
const changePasswordTitle = ref<string>($t('abp.user.changePassword'));
const [ChangePasswordModal, changePasswordModalApi] = useVbenModal({
draggable: false,
// centered: true,
showCancelButton: false,
showConfirmButton: false,
closeOnPressEscape: false,
closeOnClickModal: false,
closable: false,
fullscreenButton: false,
title: changePasswordTitle.value,
onConfirm: () => {},
onBeforeClose: () => true,
});
const notifications = ref<NotificationItem[]>([]); const notifications = ref<NotificationItem[]>([]);
const { startConnect, closeConnect } = useSignalR();
function convertToNotificationItem(message: any): NotificationItem { function convertToNotificationItem(message: any): NotificationItem {
return { return {
avatar: '', avatar: '',
@ -48,22 +61,24 @@ const eventbus = useEventbus();
const userStore = useUserStore(); const userStore = useUserStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const router = useRouter(); // const router = useRouter();
const { refresh } = useRefresh(); const { refresh } = useRefresh();
const { startConnect, closeConnect } = useSignalR();
onMounted(async () => { onMounted(async () => {
// // //
if ( // if (userStore.checkUserLoginExpire()) {
userStore?.userInfo?.id || // //
userStore?.userInfo?.token || // await router.replace({
userStore.checkUserLoginExpire() // path: LOGIN_PATH,
) { // query: {
// // redirect: encodeURIComponent(router.currentRoute.value.fullPath),
await router.replace({ // },
path: LOGIN_PATH, // });
query: { // }
redirect: encodeURIComponent(router.currentRoute.value.fullPath), const result = await postUsersNeedChangePassword();
}, if (result.data?.needChangePassword) {
}); changePasswordTitle.value = result.data.message as string;
changePasswordModalApi.open();
} }
// SignalR // SignalR
await startConnect(); await startConnect();
@ -81,6 +96,7 @@ onMounted(async () => {
// await authStore.getApplicationConfiguration(); // await authStore.getApplicationConfiguration();
await refresh(); await refresh();
} }
eventbus.subscribe('ReceiveTextMessageHandlerAsync', (content) => { eventbus.subscribe('ReceiveTextMessageHandlerAsync', (content) => {
const item: NotificationItem = { const item: NotificationItem = {
avatar: '', avatar: '',
@ -97,6 +113,7 @@ onMounted(async () => {
}); });
onBeforeUnmount(async () => { onBeforeUnmount(async () => {
await closeConnect(); await closeConnect();
eventbus.clear();
}); });
const { destroyWatermark, updateWatermark } = useWatermark(); const { destroyWatermark, updateWatermark } = useWatermark();
const showDot = computed(() => const showDot = computed(() =>
@ -107,11 +124,6 @@ const [MyProfileModal, myProfileModalApi] = useVbenModal({
onConfirm: () => {}, onConfirm: () => {},
onBeforeClose: () => {}, onBeforeClose: () => {},
}); });
const [NotifyItemModal, notifyItemModalApi] = useVbenModal({
draggable: true,
onConfirm: () => {},
onBeforeClose: () => {},
});
const menus = computed(() => [ const menus = computed(() => [
{ {
handler: () => { handler: () => {
@ -142,7 +154,7 @@ async function handleMakeAll() {
if (readIds.length > 0) { if (readIds.length > 0) {
await postNotificationBatchRead({ body: { ids: readIds } }); await postNotificationBatchRead({ body: { ids: readIds } });
Message.success($t('common.editSuccess')); Message.success($t('common.success'));
} }
notifications.value.forEach((item) => (item.isRead = true)); notifications.value.forEach((item) => (item.isRead = true));
@ -160,25 +172,6 @@ async function handleRead(row: NotificationItem) {
} }
}); });
} }
function handleViewAll() {
notifyItemModalApi.open();
}
watch(
() => preferences.app.watermark,
async (enable) => {
if (enable) {
await updateWatermark({
content: `${userStore.userInfo?.userName}`,
});
} else {
destroyWatermark();
}
},
{
immediate: true,
},
);
async function loadMessage() { async function loadMessage() {
notifications.value = []; notifications.value = [];
@ -201,6 +194,29 @@ async function loadMessage() {
}); });
}); });
} }
watch(
() => preferences.app.watermark,
async (enable) => {
if (enable) {
await updateWatermark({
content: `${userStore.userInfo?.userName}`,
});
} else {
destroyWatermark();
}
},
{
immediate: true,
},
);
const [NotifyItemModal, notifyItemModalApi] = useVbenModal({
draggable: true,
onConfirm: () => {},
onBeforeClose: () => {},
});
function handleViewAll() {
notifyItemModalApi.open();
}
</script> </script>
<template> <template>
@ -237,6 +253,7 @@ async function loadMessage() {
<LockScreen :avatar @to-login="handleLogout" /> <LockScreen :avatar @to-login="handleLogout" />
</template> </template>
</BasicLayout> </BasicLayout>
<MyProfileModal <MyProfileModal
:show-cancel-button="false" :show-cancel-button="false"
:show-confirm-button="false" :show-confirm-button="false"
@ -253,4 +270,11 @@ async function loadMessage() {
> >
<NotifyItem /> <NotifyItem />
</NotifyItemModal> </NotifyItemModal>
<ChangePasswordModal>
<ChangePassword
:message="changePasswordTitle"
@close="changePasswordModalApi.close"
/>
</ChangePasswordModal>
</template> </template>

View File

@ -0,0 +1,108 @@
<script setup lang="ts">
import { defineProps } from 'vue';
import { z } from '@vben/common-ui';
import { ElMessage as Message } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import { postUsersChangePassword } from '#/api-client';
import { $t } from '#/locales';
interface Props {
message: string;
}
defineOptions({
name: 'ChangePassword',
});
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<{
close: any;
}>();
const [ResetPasswordForm, resetPasswordApi] = useVbenForm({
//
collapsed: false,
//
commonConfig: {
//
componentProps: {
class: 'w-4/5',
},
},
//
handleSubmit: async () => {
//
const { valid } = await resetPasswordApi.validate();
if (!valid) return;
const formValues = await resetPasswordApi.getValues();
if (formValues.currentPassword === formValues.confirmPassword) {
Message.warning($t('abp.user.newPasswordAndCurrentPasswordNotAlike'));
return;
}
if (formValues.newPassword !== formValues.confirmPassword) {
Message.warning($t('abp.user.newPasswordAndConfirmPasswordNotMatch'));
return;
}
await postUsersChangePassword({ body: formValues });
Message.success($t('abp.user.changePassword') + $t('common.success'));
await resetPasswordApi.resetForm();
emit('close');
},
layout: 'horizontal',
schema: [
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.currentPassword'),
},
fieldName: 'currentPassword',
label: $t('abp.user.currentPassword'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.currentPassword'),
}),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.newPassword'),
},
fieldName: 'newPassword',
label: $t('abp.user.newPassword'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.newPassword'),
}),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.comfirmPassword'),
},
fieldName: 'confirmPassword',
label: $t('abp.user.comfirmPassword'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.comfirmPassword'),
}),
},
],
resetButtonOptions: {
show: false,
},
submitButtonOptions: {
content: '确认',
},
wrapperClass: 'grid-cols-1',
});
</script>
<template>
<div>
<div
class="flex w-full justify-center"
style="margin-bottom: 20px; color: red"
>
{{ message }}
</div>
<ResetPasswordForm />
</div>
</template>

View File

@ -0,0 +1,400 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { z } from '@vben/common-ui';
import {
ElButton as Button,
ElCol as Col,
ElImage as Image,
ElMessage as Message,
ElRow as Row,
ElStep as Step,
ElSteps as Steps,
ElTabPane as TabPane,
ElTabs as Tabs,
} from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
postUsersCanUseTwoFactor,
postUsersChangePassword,
postUsersDisabledTwoFactor,
postUsersEnabledTwoFactor,
postUsersGetQrCode,
postUsersMyProfile,
} from '#/api-client';
import { $t } from '#/locales';
defineOptions({
name: 'MyProfile',
});
const activeName = ref(0);
const loading = ref(false);
const [ProfileForm, profileFormApi] = useVbenForm({
//
collapsed: false,
//
commonConfig: {
//
componentProps: {
class: 'w-4/5',
},
},
//
handleSubmit: () => {},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.userName'),
},
fieldName: 'userName',
label: $t('abp.user.userName'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.userName'),
}),
disabled: true,
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.name'),
},
fieldName: 'name',
disabled: true,
label: $t('abp.user.name'),
},
// {
// component: 'VbenInput',
// componentProps: {
// placeholder: $t('common.pleaseInput') + $t('abp.user.surname'),
// },
// fieldName: 'surname',
// label: $t('abp.user.surname'),
// rules: z.string().min(1, {
// message: $t('common.pleaseInput') + $t('abp.user.surname'),
// }),
// },
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.email'),
},
fieldName: 'email',
disabled: true,
label: $t('abp.user.email'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.email'),
}),
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.phone'),
},
fieldName: 'phoneNumber',
disabled: true,
label: $t('abp.user.phone'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.phone'),
}),
},
],
// showCollapseButton: false,
// showDefaultActions: false,
wrapperClass: 'grid-cols-1',
resetButtonOptions: {
show: false,
},
submitButtonOptions: {
show: false,
},
});
const [ResetPasswordForm, resetPasswordApi] = useVbenForm({
//
collapsed: false,
//
commonConfig: {
//
componentProps: {
class: 'w-4/5',
},
},
//
handleSubmit: async () => {
//
const { valid } = await resetPasswordApi.validate();
if (!valid) return;
const formValues = await resetPasswordApi.getValues();
if (formValues.currentPassword === formValues.confirmPassword) {
Message.warn($t('abp.user.newPasswordAndCurrentPasswordNotAlike'));
return;
}
if (formValues.newPassword !== formValues.confirmPassword) {
Message.warn($t('abp.user.newPasswordAndConfirmPasswordNotMatch'));
return;
}
await postUsersChangePassword({ body: formValues });
Message.success($t('common.editSuccess'));
await resetPasswordApi.resetForm();
},
layout: 'horizontal',
schema: [
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.currentPassword'),
},
fieldName: 'currentPassword',
label: $t('abp.user.currentPassword'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.currentPassword'),
}),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.newPassword'),
},
fieldName: 'newPassword',
label: $t('abp.user.newPassword'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.newPassword'),
}),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.comfirmPassword'),
},
fieldName: 'confirmPassword',
label: $t('abp.user.comfirmPassword'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.comfirmPassword'),
}),
},
],
resetButtonOptions: {
show: false,
},
submitButtonOptions: {
content: '确认',
},
wrapperClass: 'grid-cols-1',
});
const [EnabledTwoFactorForm, enabledTwoFactorApi] = useVbenForm({
//
collapsed: false,
//
commonConfig: {
//
componentProps: {
class: 'w-4/5',
},
},
//
handleSubmit: () => {},
layout: 'vertical',
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.code'),
},
fieldName: 'code',
label: $t('abp.user.code'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.code'),
}),
},
],
resetButtonOptions: {
show: false,
},
submitButtonOptions: {
show: false,
},
wrapperClass: 'grid-cols-1',
});
const [DisabledTwoFactorForm, disabledTwoFactorApi] = useVbenForm({
//
collapsed: false,
//
commonConfig: {
//
componentProps: {
class: 'w-4/5',
},
},
//
handleSubmit: () => {},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.code'),
},
fieldName: 'code',
label: $t('abp.user.code'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.code'),
}),
},
],
resetButtonOptions: {
show: false,
},
submitButtonOptions: {
show: false,
},
wrapperClass: 'grid-cols-1',
});
onMounted(async () => {
try {
loading.value = true;
const resp = await postUsersMyProfile();
await profileFormApi.setValues({ ...resp.data });
} finally {
loading.value = false;
}
});
const currentTab = ref(0);
const qrCode = ref('data:image/png;base64,');
const secret = ref(''); //
const enableTwoFactor = ref(false); //
async function activeChange(e: any) {
if (e !== '2') return;
loading.value = true;
//
const canUseTwoFactor = await postUsersCanUseTwoFactor();
enableTwoFactor.value = canUseTwoFactor.data as boolean;
if (!enableTwoFactor.value) {
//
const qrCodeRes = await postUsersGetQrCode();
qrCode.value = `data:image/png;base64,${qrCodeRes.data?.qrCode}` as string;
secret.value = qrCodeRes.data?.secret as string;
}
loading.value = false;
}
/**
* 开启双因素验证
*/
async function enabledTwoFactor() {
try {
loading.value = true;
const validResult = await enabledTwoFactorApi.validate();
if (!validResult.valid) {
Message.warning($t('common.pleaseInput') + $t('abp.user.code'));
return;
}
const formValues = await enabledTwoFactorApi.getValues();
await postUsersEnabledTwoFactor({
body: {
...formValues,
secret: secret.value,
},
});
enableTwoFactor.value = true;
Message.info($t('abp.user.twoFactorEnabled'));
} finally {
await enabledTwoFactorApi.resetForm();
loading.value = false;
}
}
/**
* 关闭双因素验证
*/
async function disabledTwoFactor() {
try {
loading.value = true;
const validResult = await disabledTwoFactorApi.validate();
if (!validResult.valid) {
Message.warning($t('common.pleaseInput') + $t('abp.user.code'));
return;
}
const formValues = await disabledTwoFactorApi.getValues();
await postUsersDisabledTwoFactor({
body: {
...formValues,
},
});
Message.info($t('abp.user.twoFactorDisabled'));
} finally {
await disabledTwoFactorApi.resetForm();
enableTwoFactor.value = false;
loading.value = false;
await activeChange(2);
}
}
</script>
<template>
<div v-loading="loading">
<div class="bg-card px-8">
<Tabs
v-model:active-key="activeName"
tab-position="left"
@tab-change="activeChange"
>
<TabPane :key="0" :label="$t('abp.user.myProfile')">
<ProfileForm />
</TabPane>
<TabPane :key="1" :label="$t('abp.user.changePassword')">
<ResetPasswordForm />
</TabPane>
<TabPane :key="2" :label="$t('abp.user.twoFactor')">
<div v-show="enableTwoFactor">
<div style="margin-bottom: 5%">
{{ $t('abp.user.twoFactorEnabledDesc') }}
</div>
<DisabledTwoFactorForm />
<Button
style="margin-left: 72%"
type="primary"
@click="disabledTwoFactor"
>
{{ $t('common.disabled') }}
</Button>
</div>
<div v-show="!enableTwoFactor" class="mx-auto max-w-lg">
<Steps :current="currentTab" class="steps">
<Step title="Authenticator" />
<Step :title="$t('abp.user.verifyAuthenticator')" />
</Steps>
<div style="margin-top: 30px; margin-left: 20px">
<Row>
<Col :span="12">
<Image :src="qrCode" :width="200" />
</Col>
<Col :span="12">
<div>
{{ $t('abp.user.twoFactorDesc') }}
<EnabledTwoFactorForm />
<Button
style="margin-left: 55%"
type="primary"
@click="enabledTwoFactor"
>
{{ $t('common.enabled') }}
</Button>
</div>
</Col>
</Row>
</div>
</div>
</TabPane>
</Tabs>
</div>
</div>
</template>

View File

@ -1,4 +1,4 @@
import type { Locale } from 'ant-design-vue/es/locale'; import type { Language } from 'element-plus/es/locale';
import type { App } from 'vue'; import type { App } from 'vue';
@ -13,11 +13,11 @@ import {
} from '@vben/locales'; } from '@vben/locales';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import enLocale from 'element-plus/es/locale/lang/en';
import defaultLocale from 'element-plus/es/locale/lang/zh-cn';
const antdLocale = ref<Locale>(antdDefaultLocale); const elementLocale = ref<Language>(defaultLocale);
const modules = import.meta.glob('./langs/**/*.json'); const modules = import.meta.glob('./langs/**/*.json');
@ -43,7 +43,7 @@ async function loadMessages(lang: SupportedLanguagesType) {
* @param lang * @param lang
*/ */
async function loadThirdPartyMessage(lang: SupportedLanguagesType) { async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]); await Promise.all([loadElementLocale(lang), loadDayjsLocale(lang)]);
} }
/** /**
@ -74,17 +74,17 @@ async function loadDayjsLocale(lang: SupportedLanguagesType) {
} }
/** /**
* antd的语言包 * element-plus的语言包
* @param lang * @param lang
*/ */
async function loadAntdLocale(lang: SupportedLanguagesType) { async function loadElementLocale(lang: SupportedLanguagesType) {
switch (lang) { switch (lang) {
case 'en-US': { case 'en-US': {
antdLocale.value = antdEnLocale; elementLocale.value = enLocale;
break; break;
} }
case 'zh-CN': { case 'zh-CN': {
antdLocale.value = antdDefaultLocale; elementLocale.value = defaultLocale;
break; break;
} }
} }
@ -99,4 +99,4 @@ async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
}); });
} }
export { $t, antdLocale, setupI18n }; export { $t, elementLocale, setupI18n };

View File

@ -1,6 +1,7 @@
{ {
"login": { "login": {
"selectTenant": "Please select Tenant and ignore the non-tenant mode", "selectTenant": "Please select Tenant and ignore the non-tenant mode",
"inputCode": "Please enter the two-factor authentication code. If two-factor authentication has not been enabled for your account, please ignore this message.",
"oidcTip": "Login......" "oidcTip": "Login......"
}, },
"menu": { "menu": {
@ -25,7 +26,8 @@
"code-genarate": "CodeGenarate", "code-genarate": "CodeGenarate",
"code-entity": "Entity", "code-entity": "Entity",
"code-template-detail": "TemplateDetail", "code-template-detail": "TemplateDetail",
"code-Preview": "Preview" "code-Preview": "Preview",
"file": "FileManagement"
}, },
"user": { "user": {
"user": "User", "user": "User",
@ -39,6 +41,7 @@
"newPassword": "NewPassword", "newPassword": "NewPassword",
"confirmNewPassword": "ConfirmNewPassword", "confirmNewPassword": "ConfirmNewPassword",
"changePassword": "ChangePassword", "changePassword": "ChangePassword",
"resetPassword": "ResetPassword",
"status": "Status", "status": "Status",
"comfirmPassword": "ComfirmPassword", "comfirmPassword": "ComfirmPassword",
"comfirmDeleteUser": "Are you sure you want to delete the user", "comfirmDeleteUser": "Are you sure you want to delete the user",
@ -127,5 +130,11 @@
"featureManagement": "FeatureManagement", "featureManagement": "FeatureManagement",
"connectionStringName": "ConnectionStringName", "connectionStringName": "ConnectionStringName",
"connectionString": "ConnectionString" "connectionString": "ConnectionString"
},
"file": {
"file": "File",
"name": "FileName",
"size": "Size",
"contentType": "ContentType"
} }
} }

View File

@ -33,7 +33,7 @@
"timeOut": "Request timed out!", "timeOut": "Request timed out!",
"expandAll": "EexpandAll", "expandAll": "EexpandAll",
"collapseAll": "CollapseAll", "collapseAll": "CollapseAll",
"description": "description", "pleaseSelect": "Please Select",
"comfirm": "Comfirm", "upload": "Upload",
"valid": "Valid" "download": "Download"
} }

View File

@ -0,0 +1,13 @@
{
"title": "Demos",
"elementPlus": "Element Plus",
"form": "Form",
"vben": {
"title": "Project",
"about": "About",
"document": "Document",
"antdv": "Ant Design Vue Version",
"naive-ui": "Naive UI Version",
"element-plus": "Element Plus Version"
}
}

View File

@ -0,0 +1,8 @@
{
"templateManagement": "TemplateManagement",
"templateList": "TemplateList",
"name": "Name",
"code": "Code",
"content": "Content",
"cultureName": "Language"
}

View File

@ -1,6 +1,7 @@
{ {
"login": { "login": {
"selectTenant": "请选择租户,非租户模式请忽略", "selectTenant": "请选择租户,非租户模式请忽略",
"inputCode": "请输入双因素验证码,如果账户没有开启双因素验证请忽略",
"oidcTip": "登陆中......" "oidcTip": "登陆中......"
}, },
"menu": { "menu": {
@ -25,7 +26,8 @@
"code-genarate": "代码生成", "code-genarate": "代码生成",
"code-entity": "实体", "code-entity": "实体",
"code-template-detail": "模板详情", "code-template-detail": "模板详情",
"code-Preview": "预览" "code-Preview": "预览",
"file": "文件管理"
}, },
"user": { "user": {
"user": "用户", "user": "用户",
@ -39,6 +41,7 @@
"newPassword": "新密码", "newPassword": "新密码",
"confirmNewPassword": "确认新密码", "confirmNewPassword": "确认新密码",
"changePassword": "修改密码", "changePassword": "修改密码",
"resetPassword": "重置密码",
"status": "状态", "status": "状态",
"comfirmPassword": "确认密码", "comfirmPassword": "确认密码",
"comfirmDeleteUser": "确认删除用户", "comfirmDeleteUser": "确认删除用户",
@ -127,5 +130,11 @@
"featureManagement": "功能管理", "featureManagement": "功能管理",
"connectionStringName": "连接名称", "connectionStringName": "连接名称",
"connectionString": "连接字符串" "connectionString": "连接字符串"
},
"file": {
"file": "文件",
"name": "文件名称",
"size": "文件大小",
"contentType": "文件类型"
} }
} }

View File

@ -33,7 +33,7 @@
"timeOut": "请求超时!", "timeOut": "请求超时!",
"expandAll": "展开全部", "expandAll": "展开全部",
"collapseAll": "折叠全部", "collapseAll": "折叠全部",
"description": "描述", "pleaseSelect": "请选择",
"comfirm": "确认", "upload": "上传",
"valid": "验证" "download": "下载"
} }

View File

@ -0,0 +1,13 @@
{
"title": "演示",
"elementPlus": "Element Plus",
"form": "表单演示",
"vben": {
"title": "项目",
"about": "关于",
"document": "文档",
"antdv": "Ant Design Vue 版本",
"naive-ui": "Naive UI 版本",
"element-plus": "Element Plus 版本"
}
}

View File

@ -0,0 +1,8 @@
{
"templateManagement": "模板管理",
"templateList": "模板列表",
"name": "名称",
"code": "编码",
"content": "内容",
"cultureName": "语言"
}

View File

@ -1,12 +1,10 @@
import { initPreferences } from '@vben/preferences'; import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils'; import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences';
// eslint-disable-next-line unused-imports/no-unused-imports // eslint-disable-next-line unused-imports/no-unused-imports
import client from '#/api-client-config/index'; import client from '#/api-client-config/index';
// eslint-disable-next-line unused-imports/no-unused-imports
import clientblob from '#/api-client-config/index-blob';
import { overridesPreferences } from './preferences';
/** /**
* *
*/ */

View File

@ -13,11 +13,15 @@ export const overridesPreferences = defineOverridesPreferences({
enableCheckUpdates: false, enableCheckUpdates: false,
// 检查更新的时间间隔,单位为分钟 // 检查更新的时间间隔,单位为分钟
checkUpdatesInterval: 1, checkUpdatesInterval: 1,
defaultAvatar: '/public/avatar-v1.webp', // 默认头像
}, },
theme: { theme: {
mode: 'light', mode: 'light',
}, },
copyright: { copyright: {
companyName: '集社', companyName: 'Abp Vben5 Ele',
},
logo: {
source: '/logo-v1.webp', // 网站图标
}, },
}); });

View File

@ -6,7 +6,7 @@ import type {
import { generateAccessible } from '@vben/access'; import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { message } from 'ant-design-vue'; import { ElMessage } from 'element-plus';
import { getAllMenusApi } from '#/api'; import { getAllMenusApi } from '#/api';
import { BasicLayout, IFrameView } from '#/layouts'; import { BasicLayout, IFrameView } from '#/layouts';
@ -25,9 +25,9 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
return await generateAccessible(preferences.app.accessMode, { return await generateAccessible(preferences.app.accessMode, {
...options, ...options,
fetchMenuListAsync: async () => { fetchMenuListAsync: async () => {
message.loading({ ElMessage({
content: `${$t('common.loadingMenu')}...`, duration: 1500,
duration: 1.5, message: `${$t('common.loadingMenu')}...`,
}); });
return await getAllMenusApi(); return await getAllMenusApi();
}, },

View File

@ -1,6 +1,6 @@
import type { Router } from 'vue-router'; import type { Router } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores'; import { useAccessStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils'; import { startProgress, stopProgress } from '@vben/utils';
@ -56,7 +56,7 @@ function setupAccessGuard(router: Router) {
return decodeURIComponent( return decodeURIComponent(
(to.query?.redirect as string) || (to.query?.redirect as string) ||
userStore.userInfo?.homePath || userStore.userInfo?.homePath ||
DEFAULT_HOME_PATH, preferences.app.defaultHomePath,
); );
} }
return true; return true;
@ -75,7 +75,7 @@ function setupAccessGuard(router: Router) {
path: LOGIN_PATH, path: LOGIN_PATH,
// 如不需要,直接删除 query // 如不需要,直接删除 query
query: query:
to.fullPath === DEFAULT_HOME_PATH to.fullPath === preferences.app.defaultHomePath
? {} ? {}
: { redirect: encodeURIComponent(to.fullPath) }, : { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面 // 携带当前跳转的页面,登录后重新跳转该页面
@ -89,6 +89,7 @@ function setupAccessGuard(router: Router) {
if (accessStore.isAccessChecked) { if (accessStore.isAccessChecked) {
return true; return true;
} }
// 生成路由表 // 生成路由表
// 当前登录用户拥有的角色标识列表 // 当前登录用户拥有的角色标识列表
// const userInfo = userStore.userInfo || (await authStore.fetchUserInfo()); // const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
@ -97,7 +98,6 @@ function setupAccessGuard(router: Router) {
if (import.meta.env.VITE_REFRESH_ROLE && refreshCount > 0) { if (import.meta.env.VITE_REFRESH_ROLE && refreshCount > 0) {
await authStore.getApplicationConfiguration(); await authStore.getApplicationConfiguration();
} }
const userRoles = accessStore.accessCodes ?? []; const userRoles = accessStore.accessCodes ?? [];
// 生成菜单和路由 // 生成菜单和路由
@ -113,8 +113,8 @@ function setupAccessGuard(router: Router) {
accessStore.setAccessRoutes(accessibleRoutes); accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true); accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ?? const redirectPath = (from.query.redirect ??
(to.path === DEFAULT_HOME_PATH (to.path === preferences.app.defaultHomePath
? DEFAULT_HOME_PATH ? userStore.userInfo?.homePath || preferences.app.defaultHomePath
: to.fullPath)) as string; : to.fullPath)) as string;
return { return {

View File

@ -1,11 +1,12 @@
import type { RouteRecordRaw } from 'vue-router'; import type { RouteRecordRaw } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { AuthPageLayout, BasicLayout } from '#/layouts';
import { $t } from '#/locales'; import { $t } from '#/locales';
import Login from '#/views/_core/authentication/login.vue';
const BasicLayout = () => import('#/layouts/basic.vue');
const AuthPageLayout = () => import('#/layouts/auth.vue');
/** 全局404页面 */ /** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = { const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'), component: () => import('#/views/_core/fallback/not-found.vue'),
@ -34,7 +35,7 @@ const coreRoutes: RouteRecordRaw[] = [
}, },
name: 'Root', name: 'Root',
path: '/', path: '/',
redirect: DEFAULT_HOME_PATH, redirect: preferences.app.defaultHomePath,
children: [], children: [],
}, },
{ {
@ -50,7 +51,7 @@ const coreRoutes: RouteRecordRaw[] = [
{ {
name: 'Login', name: 'Login',
path: 'login', path: 'login',
component: Login, component: () => import('#/views/_core/authentication/login.vue'),
meta: { meta: {
title: $t('page.auth.login'), title: $t('page.auth.login'),
}, },
@ -72,6 +73,14 @@ const coreRoutes: RouteRecordRaw[] = [
title: $t('page.auth.qrcodeLogin'), title: $t('page.auth.qrcodeLogin'),
}, },
}, },
{
name: 'OidcLogin',
path: 'oidc-login',
component: () => import('#/views/_core/authentication/oidc-login.vue'),
meta: {
title: $t('page.auth.thirdPartyLogin'),
},
},
{ {
name: 'ForgetPassword', name: 'ForgetPassword',
path: 'forget-password', path: 'forget-password',

View File

@ -0,0 +1,29 @@
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
title: $t('demos.title'),
},
name: 'Demos',
path: '/demos',
children: [
{
meta: {
title: $t('demos.elementPlus'),
icon: 'logos:element',
},
name: 'NaiveDemos',
path: '/demos/element',
component: () => import('#/views/demos/element/index.vue'),
},
],
},
];
export default routes;

View File

@ -0,0 +1,32 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: 'ant-design:folder-open-outlined',
order: 3,
title: $t('abp.menu.file'),
authority: ['FileManagement'],
},
name: 'file',
path: '/file',
children: [
{
name: 'abpFile',
path: 'page',
component: () => import('#/views/system/abpfiles/index.vue'),
meta: {
icon: 'ant-design:file-text-twotone',
title: $t('abp.menu.file'),
authority: ['FileManagement.File'],
},
},
],
},
];
export default routes;

View File

@ -66,6 +66,16 @@ const routes: RouteRecordRaw[] = [
authority: ['AbpIdentity.FeatureManagement'], authority: ['AbpIdentity.FeatureManagement'],
}, },
}, },
{
name: 'DataDictionary',
path: 'data-dictionary',
component: () => import('#/views/system/abpdatadictionary/index.vue'),
meta: {
title: $t('abp.menu.dataDictionary'),
authority: ['AbpIdentity.DataDictionaryManagement'],
icon: 'ant-design:table-outlined',
},
},
{ {
name: 'AbpAuditLog', name: 'AbpAuditLog',
path: 'auditlog', path: 'auditlog',
@ -106,16 +116,6 @@ const routes: RouteRecordRaw[] = [
icon: 'ant-design:font-size-outlined', icon: 'ant-design:font-size-outlined',
}, },
}, },
{
name: 'DataDictionary',
path: 'data-dictionary',
component: () => import('#/views/system/abpdatadictionary/index.vue'),
meta: {
title: $t('abp.menu.dataDictionary'),
authority: ['AbpIdentity.DataDictionaryManagement'],
icon: 'ant-design:table-outlined',
},
},
{ {
name: 'AbpNotification', name: 'AbpNotification',
path: 'notification', path: 'notification',

View File

@ -0,0 +1,32 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: 'ant-design:tool-outlined',
// order: 998,
title: $t('textTemplate.templateManagement'),
authority: ['AbpTemplateManagement'],
},
name: 'TextTemplate',
path: '/TextTemplate',
children: [
{
path: 'page',
name: 'TextTemplatePage',
component: () => import('#/views/textTemplate/index.vue'),
meta: {
icon: 'ant-design:file-markdown-filled',
title: $t('textTemplate.templateList'),
authority: ['AbpTemplateManagement.Template'],
},
},
],
},
];
export default routes;

View File

@ -0,0 +1,91 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
badgeType: 'dot',
icon: 'ph:file-doc-light',
order: 9999,
title: $t('demos.vben.title'),
},
name: 'VbenProject',
path: '/vben-admin',
children: [
// {
// name: 'VbenAbout',
// path: '/vben-admin/about',
// component: () => import('#/views/_core/about/index.vue'),
// meta: {
// icon: 'lucide:copyright',
// title: $t('demos.vben.about'),
// },
// },
{
name: 'VbenDocument',
path: '/vben-admin/document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
link: 'https://doc.cncore.club/',
title: 'ABPPro文档',
},
},
{
name: 'VbenDocument',
path: '/vben-admin/document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
link: 'http://doc.china.cncore.club:81/',
title: 'ABPPro国内文档',
},
},
{
name: 'VbenDocument',
path: '/vben-admin/document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
link: 'https://abp.io/docs/latest/',
title: 'ABP官方文档',
},
},
{
name: 'VbenGithub',
path: '/vben-admin/github',
component: IFrameView,
meta: {
icon: 'mdi:github',
link: 'https://github.com/WangJunZzz/abp-vnext-pro',
title: 'Github',
},
},
{
name: 'VbenGithub',
path: '/vben-admin/github',
component: IFrameView,
meta: {
icon: 'ant-design:google-circle-filled',
link: 'https://gitee.com/WangJunZzz/abp-vnext-pro',
title: 'Gitee',
},
},
{
name: 'VbenGithub',
path: '/vben-admin/github',
component: IFrameView,
meta: {
icon: 'logos:element',
link: 'https://element-plus-docs.bklab.cn/zh-CN/',
title: 'Element Plus',
},
},
],
},
];
export default routes;

View File

@ -1,22 +1,26 @@
import type { Recordable, UserInfo } from '@vben/types'; import type { Recordable, UserInfo } from '@vben/types';
import type {
ApplicationAuthConfigurationDto,
ApplicationConfigurationDto,
} from '#/api-client';
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { message as Message, notification } from 'ant-design-vue'; import { ElNotification, ElMessage as Message } from 'element-plus';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { getUserInfoApi, logoutApi } from '#/api';
import { import {
type ApplicationAuthConfigurationDto,
type ApplicationConfigurationDto,
getApiAbpApplicationConfiguration, getApiAbpApplicationConfiguration,
postApiAppAccountLogin, postApiAppAccountLogin2Fa,
postTenantsFind, postTenantsFind,
} from '#/api-client'; } from '#/api-client';
import { useSignalR } from '#/hooks/useSignalR';
import { $t } from '#/locales'; import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
@ -37,9 +41,7 @@ export const useAuthStore = defineStore('auth', () => {
) { ) {
// 异步处理用户登录操作并获取 accessToken // 异步处理用户登录操作并获取 accessToken
let userInfo: null | UserInfo = null; let userInfo: null | UserInfo = null;
try { try {
loginLoading.value = true;
// 判断是否租户登录 // 判断是否租户登录
if (params.tenant) { if (params.tenant) {
const tenantResult = await postTenantsFind({ const tenantResult = await postTenantsFind({
@ -47,18 +49,17 @@ export const useAuthStore = defineStore('auth', () => {
name: params.tenant, name: params.tenant,
}, },
}); });
if (tenantResult.data?.success) { if (tenantResult.data?.success) {
userStore.setTenantInfo(tenantResult.data as any); userStore.setTenantInfo(tenantResult.data as any);
} else { } else {
Message.error(`${params.tenant}$t('abp.tenant.notExist')`);
userStore.setTenantInfo(null); userStore.setTenantInfo(null);
delete params.tenant; Message.error($t('abp.tenant.notExist'));
return; return;
} }
} }
const { data = {} } = await postApiAppAccountLogin({ loginLoading.value = true;
const { data = {} } = await postApiAppAccountLogin2Fa({
body: { body: {
...params, ...params,
}, },
@ -66,22 +67,25 @@ export const useAuthStore = defineStore('auth', () => {
// 如果成功获取到 accessToken // 如果成功获取到 accessToken
if (data.token) { if (data.token) {
accessStore.setAccessToken(data.token); accessStore.setAccessToken(data.token);
accessStore.setRefreshToken(data.refreshToken as string);
userInfo = data as any; userInfo = data as any;
userStore.setUserInfo(userInfo as any); userStore.setUserInfo(userInfo as any);
await getApplicationConfiguration(); await getApplicationConfiguration();
if (accessStore.loginExpired) { if (accessStore.loginExpired) {
accessStore.setLoginExpired(false); accessStore.setLoginExpired(false);
} else { } else {
onSuccess onSuccess
? await onSuccess?.() ? await onSuccess?.()
: await router.push(DEFAULT_HOME_PATH); : await router.push(
userInfo?.homePath || preferences.app.defaultHomePath,
);
} }
if (userInfo?.userName) { if (userInfo?.userName) {
notification.success({ ElNotification({
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.userName}`, message: `${$t('authentication.loginSuccessDesc')}:${userInfo?.userName}`,
duration: 3, title: $t('authentication.loginSuccess'),
message: $t('authentication.loginSuccess'), type: 'success',
}); });
} }
} }
@ -99,9 +103,7 @@ export const useAuthStore = defineStore('auth', () => {
async function logout(redirect: boolean = true) { async function logout(redirect: boolean = true) {
try { try {
// await logoutApi(); await logoutApi();
const { closeConnect } = useSignalR();
closeConnect();
} catch { } catch {
// 不做任何处理 // 不做任何处理
} }
@ -120,16 +122,15 @@ export const useAuthStore = defineStore('auth', () => {
} }
async function fetchUserInfo() { async function fetchUserInfo() {
const userInfo: null | UserInfo = null; let userInfo: null | UserInfo = null;
// userInfo = await getUserInfoApi(); userInfo = await getUserInfoApi();
// userStore.setUserInfo(userInfo); userStore.setUserInfo(userInfo);
return userInfo; return userInfo;
} }
function $reset() { function $reset() {
loginLoading.value = false; loginLoading.value = false;
} }
async function getApplicationConfiguration() { async function getApplicationConfiguration() {
const { data: authData } = await getApiAbpApplicationConfiguration({ const { data: authData } = await getApiAbpApplicationConfiguration({
query: { IncludeLocalizationResources: false }, query: { IncludeLocalizationResources: false },

View File

@ -6,7 +6,7 @@ import { computed, onBeforeMount, ref } from 'vue';
import { AuthenticationLogin, z } from '@vben/common-ui'; import { AuthenticationLogin, z } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { getApiAbpApplicationConfiguration } from '#/api-client/index'; import { getApiAppAbpProBasicApplicationConfiguration } from '#/api-client/index';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
defineOptions({ name: 'Login' }); defineOptions({ name: 'Login' });
@ -15,7 +15,6 @@ const authStore = useAuthStore();
const showThirdPartyLogin = ref(false); const showThirdPartyLogin = ref(false);
const thirdPartLoginList = ref([]); const thirdPartLoginList = ref([]);
const showTenant = ref(false); const showTenant = ref(false);
const formSchema = computed((): VbenFormSchema[] => { const formSchema = computed((): VbenFormSchema[] => {
return [ return [
{ {
@ -31,6 +30,7 @@ const formSchema = computed((): VbenFormSchema[] => {
componentProps: { componentProps: {
placeholder: $t('authentication.usernameTip'), placeholder: $t('authentication.usernameTip'),
}, },
fieldName: 'name', fieldName: 'name',
label: $t('authentication.username'), label: $t('authentication.username'),
rules: z rules: z
@ -38,6 +38,7 @@ const formSchema = computed((): VbenFormSchema[] => {
.min(1, { message: $t('authentication.usernameTip') }) .min(1, { message: $t('authentication.usernameTip') })
.default('admin'), .default('admin'),
}, },
{ {
component: 'VbenInputPassword', component: 'VbenInputPassword',
componentProps: { componentProps: {
@ -50,16 +51,23 @@ const formSchema = computed((): VbenFormSchema[] => {
.min(1, { message: $t('authentication.passwordTip') }) .min(1, { message: $t('authentication.passwordTip') })
.default('1q2w3E*'), .default('1q2w3E*'),
}, },
{
component: 'VbenInput',
componentProps: {
placeholder: $t('abp.login.inputCode'),
},
fieldName: 'code',
label: $t('abp.user.code'),
},
]; ];
}); });
onBeforeMount(async () => { onBeforeMount(async () => {
const result = await getApiAbpApplicationConfiguration(); const result = await getApiAppAbpProBasicApplicationConfiguration();
showThirdPartyLogin.value = result.data?.oidcConfiguration showThirdPartyLogin.value = result.data?.oidcConfiguration
?.enabled as boolean; ?.enabled as boolean;
thirdPartLoginList.value = result.data?.oidcConfiguration thirdPartLoginList.value = result.data?.oidcConfiguration
?.oidcConfiguration as []; ?.oidcConfiguration as [];
//
showTenant.value = result.data?.multiTenancy?.isEnabled ?? false; showTenant.value = result.data?.multiTenancy?.isEnabled ?? false;
}); });
</script> </script>

View File

@ -0,0 +1,55 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { ElNotification } from 'element-plus';
import { postApiAppAccountLoginOidc } from '#/api-client';
import { $t } from '#/locales';
import { useAuthStore } from '#/store';
defineOptions({ name: 'OidcLogin' });
const loading = ref(true);
const tip = ref($t('abp.login.oidcTip'));
const router = useRouter();
const { currentRoute } = useRouter();
const accessStore = useAccessStore();
const userStore = useUserStore();
const code = currentRoute.value.query.code as string;
const state = currentRoute.value.query.state as string;
const authStore = useAuthStore();
onMounted(async () => {
try {
// oidc
const result = await postApiAppAccountLoginOidc({ body: { code, state } });
accessStore.setAccessToken(result.data?.token as string);
userStore.setUserInfo(result.data as any);
await authStore.getApplicationConfiguration();
await router.push(
userStore.userInfo?.homePath || preferences.app.defaultHomePath,
);
if (result.data?.userName) {
ElNotification({
message: `${$t('authentication.loginSuccessDesc')}:${result.data?.userName}`,
title: $t('authentication.loginSuccess'),
type: 'success',
});
}
} catch {
await router.push(LOGIN_PATH);
} finally {
loading.value = false;
}
});
</script>
<template>
<div>
{{ tip }}
</div>
</template>

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