初始化处理
This commit is contained in:
parent
039eeb6670
commit
cbde1b330f
@ -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
|
||||||
|
|||||||
@ -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',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@ -1 +1 @@
|
|||||||
20.14.0
|
22.1.0
|
||||||
|
|||||||
2
.npmrc
2
.npmrc
@ -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
20
.vscode/settings.json
vendored
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
# 应用标题
|
|
||||||
VITE_APP_TITLE=采集端综合管理
|
|
||||||
|
|
||||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
|
||||||
VITE_APP_NAMESPACE=abp-vnext-pro-vben5-antd
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
/**
|
|
||||||
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
|
||||||
* 可用于 vben-form、vben-modal、vben-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 };
|
|
||||||
@ -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;
|
|
||||||
@ -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
@ -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>
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export { createLoading } from './src/createLoading';
|
|
||||||
export { default as Loading } from './src/Loading.vue';
|
|
||||||
|
|
||||||
export { useLoading } from './src/useLoading';
|
|
||||||
@ -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>
|
|
||||||
@ -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;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -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';
|
|
||||||
}
|
|
||||||
@ -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];
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
@ -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": "数据类型"
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -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
8
apps/web-ele/.env
Normal 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
|
||||||
@ -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
|
||||||
|
|
||||||
|
# 是否打开 devtools,true 为打开,false 为关闭
|
||||||
|
VITE_DEVTOOLS=false
|
||||||
|
|
||||||
# vue-router 的模式
|
# vue-router 的模式
|
||||||
VITE_ROUTER_HISTORY=history
|
VITE_ROUTER_HISTORY=history
|
||||||
|
|
||||||
|
|
||||||
# 是否打开 devtools,true 为打开,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
|
||||||
@ -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
|
||||||
@ -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);
|
||||||
})();
|
})();
|
||||||
@ -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:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
apps/web-ele/public/avatar-v1.webp
Normal file
BIN
apps/web-ele/public/avatar-v1.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
BIN
apps/web-ele/public/logo-v1.webp
Normal file
BIN
apps/web-ele/public/logo-v1.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
338
apps/web-ele/src/adapter/component/index.ts
Normal file
338
apps/web-ele/src/adapter/component/index.ts
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
/**
|
||||||
|
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
||||||
|
* 可用于 vben-form、vben-modal、vben-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 };
|
||||||
@ -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]);
|
||||||
@ -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 },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -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',
|
||||||
});
|
});
|
||||||
146
apps/web-ele/src/api-client-config/index.ts
Normal file
146
apps/web-ele/src/api-client-config/index.ts
Normal 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 it is too large
Load Diff
1479
apps/web-ele/src/api-client/services.gen.ts
Normal file
1479
apps/web-ele/src/api-client/services.gen.ts
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -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
17
apps/web-ele/src/app.vue
Normal 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>
|
||||||
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
18
apps/web-ele/src/components/icon/icon.vue
Normal file
18
apps/web-ele/src/components/icon/icon.vue
Normal 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>
|
||||||
201
apps/web-ele/src/components/table-action/table-action.vue
Normal file
201
apps/web-ele/src/components/table-action/table-action.vue
Normal 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>
|
||||||
@ -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;
|
||||||
23
apps/web-ele/src/hooks/useLoadIcon.ts
Normal file
23
apps/web-ele/src/hooks/useLoadIcon.ts
Normal 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);
|
||||||
@ -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',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
108
apps/web-ele/src/layouts/change-password.vue
Normal file
108
apps/web-ele/src/layouts/change-password.vue
Normal 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>
|
||||||
400
apps/web-ele/src/layouts/my-profile.vue
Normal file
400
apps/web-ele/src/layouts/my-profile.vue
Normal 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>
|
||||||
@ -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 };
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
13
apps/web-ele/src/locales/langs/en-US/demos.json
Normal file
13
apps/web-ele/src/locales/langs/en-US/demos.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
apps/web-ele/src/locales/langs/en-US/textTemplate.json
Normal file
8
apps/web-ele/src/locales/langs/en-US/textTemplate.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"templateManagement": "TemplateManagement",
|
||||||
|
"templateList": "TemplateList",
|
||||||
|
"name": "Name",
|
||||||
|
"code": "Code",
|
||||||
|
"content": "Content",
|
||||||
|
"cultureName": "Language"
|
||||||
|
}
|
||||||
@ -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": "文件类型"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,7 +33,7 @@
|
|||||||
"timeOut": "请求超时!",
|
"timeOut": "请求超时!",
|
||||||
"expandAll": "展开全部",
|
"expandAll": "展开全部",
|
||||||
"collapseAll": "折叠全部",
|
"collapseAll": "折叠全部",
|
||||||
"description": "描述",
|
"pleaseSelect": "请选择",
|
||||||
"comfirm": "确认",
|
"upload": "上传",
|
||||||
"valid": "验证"
|
"download": "下载"
|
||||||
}
|
}
|
||||||
13
apps/web-ele/src/locales/langs/zh-CN/demos.json
Normal file
13
apps/web-ele/src/locales/langs/zh-CN/demos.json
Normal 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 版本"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
apps/web-ele/src/locales/langs/zh-CN/textTemplate.json
Normal file
8
apps/web-ele/src/locales/langs/zh-CN/textTemplate.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"templateManagement": "模板管理",
|
||||||
|
"templateList": "模板列表",
|
||||||
|
"name": "名称",
|
||||||
|
"code": "编码",
|
||||||
|
"content": "内容",
|
||||||
|
"cultureName": "语言"
|
||||||
|
}
|
||||||
@ -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';
|
||||||
/**
|
/**
|
||||||
* 应用初始化完成之后再进行页面加载渲染
|
* 应用初始化完成之后再进行页面加载渲染
|
||||||
*/
|
*/
|
||||||
@ -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', // 网站图标
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -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();
|
||||||
},
|
},
|
||||||
@ -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 {
|
||||||
@ -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',
|
||||||
29
apps/web-ele/src/router/routes/modules/demos.ts
Normal file
29
apps/web-ele/src/router/routes/modules/demos.ts
Normal 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;
|
||||||
32
apps/web-ele/src/router/routes/modules/file.ts
Normal file
32
apps/web-ele/src/router/routes/modules/file.ts
Normal 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;
|
||||||
@ -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',
|
||||||
32
apps/web-ele/src/router/routes/modules/textTemplate.ts
Normal file
32
apps/web-ele/src/router/routes/modules/textTemplate.ts
Normal 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;
|
||||||
91
apps/web-ele/src/router/routes/modules/vben.ts
Normal file
91
apps/web-ele/src/router/routes/modules/vben.ts
Normal 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;
|
||||||
@ -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 },
|
||||||
@ -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>
|
||||||
55
apps/web-ele/src/views/_core/authentication/oidc-login.vue
Normal file
55
apps/web-ele/src/views/_core/authentication/oidc-login.vue
Normal 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
Loading…
x
Reference in New Issue
Block a user