committed by
GitHub
237 changed files with 10915 additions and 3598 deletions
@ -1,6 +0,0 @@ |
|||
echo Start running commit-msg hook... |
|||
|
|||
# Check whether the git commit information is standardized |
|||
pnpm exec commitlint --edit "$1" |
|||
|
|||
echo Run commit-msg hook done. |
|||
@ -1,3 +0,0 @@ |
|||
# 每次 git pull 之后, 安装依赖 |
|||
|
|||
pnpm install |
|||
@ -1,7 +0,0 @@ |
|||
# update `.vscode/vben-admin.code-workspace` file |
|||
pnpm vsh code-workspace --auto-commit |
|||
|
|||
# Format and submit code according to lintstagedrc.js configuration |
|||
pnpm exec lint-staged |
|||
|
|||
echo Run pre-commit hook done. |
|||
@ -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', |
|||
], |
|||
}; |
|||
@ -0,0 +1,30 @@ |
|||
{ |
|||
"recommendations": [ |
|||
// Vue 3 的语言支持 |
|||
"Vue.volar", |
|||
// 将 ESLint JavaScript 集成到 VS Code 中。 |
|||
"dbaeumer.vscode-eslint", |
|||
// Visual Studio Code 的官方 Stylelint 扩展 |
|||
"stylelint.vscode-stylelint", |
|||
// 使用 Prettier 的代码格式化程序 |
|||
"esbenp.prettier-vscode", |
|||
// 支持 dotenv 文件语法 |
|||
"mikestead.dotenv", |
|||
// 源代码的拼写检查器 |
|||
"streetsidesoftware.code-spell-checker", |
|||
// Tailwind CSS 的官方 VS Code 插件 |
|||
"bradlc.vscode-tailwindcss", |
|||
// iconify 图标插件 |
|||
"antfu.iconify", |
|||
// i18n 插件 |
|||
"Lokalise.i18n-ally", |
|||
// CSS 变量提示 |
|||
"vunguyentuan.vscode-css-variables", |
|||
// 在 package.json 中显示 PNPM catalog 的版本 |
|||
"antfu.pnpm-catalog-lens" |
|||
], |
|||
"unwantedRecommendations": [ |
|||
// 和 volar 冲突 |
|||
"octref.vetur" |
|||
] |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
{ |
|||
"import": { |
|||
"scope": "javascript,typescript", |
|||
"prefix": "im", |
|||
"body": ["import { $2 } from '$1';"], |
|||
"description": "Import a module", |
|||
}, |
|||
"export-all": { |
|||
"scope": "javascript,typescript", |
|||
"prefix": "ex", |
|||
"body": ["export * from '$1';"], |
|||
"description": "Export a module", |
|||
}, |
|||
"vue-script-setup": { |
|||
"scope": "vue", |
|||
"prefix": "<sc", |
|||
"body": [ |
|||
"<script setup lang=\"ts\">", |
|||
"const props = defineProps<{", |
|||
" modelValue?: boolean,", |
|||
"}>()", |
|||
"$1", |
|||
"</script>", |
|||
"", |
|||
"<template>", |
|||
" <div>", |
|||
" <slot/>", |
|||
" </div>", |
|||
"</template>", |
|||
], |
|||
}, |
|||
"vue-computed": { |
|||
"scope": "javascript,typescript,vue", |
|||
"prefix": "com", |
|||
"body": ["computed(() => { $1 })"], |
|||
}, |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/launchsettings.json", |
|||
"version": "0.2.0", |
|||
"configurations": [ |
|||
{ |
|||
"type": "chrome", |
|||
"name": "vben admin playground dev", |
|||
"request": "launch", |
|||
"url": "http://localhost:5555", |
|||
"env": { "NODE_ENV": "development" }, |
|||
"sourceMaps": true, |
|||
"webRoot": "${workspaceFolder}/playground" |
|||
}, |
|||
{ |
|||
"type": "chrome", |
|||
"name": "vben abp antd dev", |
|||
"request": "launch", |
|||
"url": "http://localhost:5666", |
|||
"env": { "NODE_ENV": "development" }, |
|||
"sourceMaps": true, |
|||
"webRoot": "${workspaceFolder}/apps/app-antd" |
|||
}, |
|||
{ |
|||
"type": "chrome", |
|||
"name": "vben admin antd dev", |
|||
"request": "launch", |
|||
"url": "http://localhost:5666", |
|||
"env": { "NODE_ENV": "development" }, |
|||
"sourceMaps": true, |
|||
"webRoot": "${workspaceFolder}/apps/web-antd" |
|||
}, |
|||
{ |
|||
"type": "chrome", |
|||
"name": "vben admin ele dev", |
|||
"request": "launch", |
|||
"url": "http://localhost:5777", |
|||
"env": { "NODE_ENV": "development" }, |
|||
"sourceMaps": true, |
|||
"webRoot": "${workspaceFolder}/apps/web-ele" |
|||
}, |
|||
{ |
|||
"type": "chrome", |
|||
"name": "vben admin naive dev", |
|||
"request": "launch", |
|||
"url": "http://localhost:5888", |
|||
"env": { "NODE_ENV": "development" }, |
|||
"sourceMaps": true, |
|||
"webRoot": "${workspaceFolder}/apps/web-naive" |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,241 @@ |
|||
{ |
|||
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts", |
|||
// workbench |
|||
"workbench.list.smoothScrolling": true, |
|||
"workbench.startupEditor": "newUntitledFile", |
|||
"workbench.tree.indent": 10, |
|||
"workbench.editor.highlightModifiedTabs": true, |
|||
"workbench.editor.closeOnFileDelete": true, |
|||
"workbench.editor.limit.enabled": true, |
|||
"workbench.editor.limit.perEditorGroup": true, |
|||
"workbench.editor.limit.value": 5, |
|||
|
|||
// editor |
|||
"editor.tabSize": 2, |
|||
"editor.detectIndentation": false, |
|||
"editor.cursorBlinking": "expand", |
|||
"editor.largeFileOptimizations": true, |
|||
"editor.accessibilitySupport": "off", |
|||
"editor.cursorSmoothCaretAnimation": "on", |
|||
"editor.guides.bracketPairs": "active", |
|||
"editor.inlineSuggest.enabled": true, |
|||
"editor.suggestSelection": "recentlyUsedByPrefix", |
|||
"editor.acceptSuggestionOnEnter": "smart", |
|||
"editor.suggest.snippetsPreventQuickSuggestions": false, |
|||
"editor.stickyScroll.enabled": true, |
|||
"editor.hover.sticky": true, |
|||
"editor.suggest.insertMode": "replace", |
|||
"editor.bracketPairColorization.enabled": true, |
|||
"editor.autoClosingBrackets": "beforeWhitespace", |
|||
"editor.autoClosingDelete": "always", |
|||
"editor.autoClosingOvertype": "always", |
|||
"editor.autoClosingQuotes": "beforeWhitespace", |
|||
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?", |
|||
"editor.codeActionsOnSave": { |
|||
"source.fixAll.eslint": "explicit", |
|||
"source.fixAll.stylelint": "explicit", |
|||
"source.organizeImports": "never" |
|||
}, |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode", |
|||
"[html]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[css]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[scss]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[javascript]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[typescript]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[json]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[markdown]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[jsonc]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[vue]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
// extensions |
|||
"extensions.ignoreRecommendations": true, |
|||
|
|||
// terminal |
|||
"terminal.integrated.cursorBlinking": true, |
|||
"terminal.integrated.persistentSessionReviveProcess": "never", |
|||
"terminal.integrated.tabs.enabled": true, |
|||
"terminal.integrated.scrollback": 10000, |
|||
"terminal.integrated.stickyScroll.enabled": true, |
|||
|
|||
// files |
|||
"files.eol": "\n", |
|||
"files.insertFinalNewline": true, |
|||
"files.simpleDialog.enable": true, |
|||
"files.associations": { |
|||
"*.ejs": "html", |
|||
"*.art": "html", |
|||
"**/tsconfig.json": "jsonc", |
|||
"*.json": "jsonc", |
|||
"package.json": "json" |
|||
}, |
|||
|
|||
"files.exclude": { |
|||
"**/.eslintcache": true, |
|||
"**/bower_components": true, |
|||
"**/.turbo": true, |
|||
"**/.idea": true, |
|||
"**/.vitepress": true, |
|||
"**/tmp": true, |
|||
"**/.git": true, |
|||
"**/.svn": true, |
|||
"**/.hg": true, |
|||
"**/CVS": true, |
|||
"**/.stylelintcache": true, |
|||
"**/.DS_Store": true, |
|||
"**/vite.config.mts.*": true, |
|||
"**/tea.yaml": true |
|||
}, |
|||
"files.watcherExclude": { |
|||
"**/.git/objects/**": true, |
|||
"**/.git/subtree-cache/**": true, |
|||
"**/.vscode/**": true, |
|||
"**/node_modules/**": true, |
|||
"**/tmp/**": true, |
|||
"**/bower_components/**": true, |
|||
"**/dist/**": true, |
|||
"**/yarn.lock": true |
|||
}, |
|||
|
|||
"typescript.tsserver.exclude": ["**/node_modules", "**/dist", "**/.turbo"], |
|||
|
|||
// search |
|||
"search.searchEditor.singleClickBehaviour": "peekDefinition", |
|||
"search.followSymlinks": false, |
|||
// 在使用搜索功能时,将这些文件夹/文件排除在外 |
|||
"search.exclude": { |
|||
"**/node_modules": true, |
|||
"**/*.log": true, |
|||
"**/*.log*": true, |
|||
"**/bower_components": true, |
|||
"**/dist": true, |
|||
"**/elehukouben": true, |
|||
"**/.git": true, |
|||
"**/.github": true, |
|||
"**/.gitignore": true, |
|||
"**/.svn": true, |
|||
"**/.DS_Store": true, |
|||
"**/.vitepress/cache": true, |
|||
"**/.idea": true, |
|||
"**/.vscode": false, |
|||
"**/.yarn": true, |
|||
"**/tmp": true, |
|||
"*.xml": true, |
|||
"out": true, |
|||
"dist": true, |
|||
"node_modules": true, |
|||
"CHANGELOG.md": true, |
|||
"**/pnpm-lock.yaml": true, |
|||
"**/yarn.lock": true |
|||
}, |
|||
|
|||
"debug.onTaskErrors": "debugAnyway", |
|||
"diffEditor.ignoreTrimWhitespace": false, |
|||
"npm.packageManager": "pnpm", |
|||
|
|||
"css.validate": false, |
|||
"less.validate": false, |
|||
"scss.validate": false, |
|||
|
|||
// extension |
|||
"emmet.showSuggestionsAsSnippets": true, |
|||
"emmet.triggerExpansionOnTab": false, |
|||
|
|||
"errorLens.enabledDiagnosticLevels": ["warning", "error"], |
|||
"errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"], |
|||
|
|||
"stylelint.enable": true, |
|||
"stylelint.packageManager": "pnpm", |
|||
"stylelint.validate": ["css", "less", "postcss", "scss", "vue"], |
|||
"stylelint.customSyntax": "postcss-html", |
|||
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"], |
|||
|
|||
"typescript.inlayHints.enumMemberValues.enabled": true, |
|||
"typescript.preferences.preferTypeOnlyAutoImports": true, |
|||
"typescript.preferences.includePackageJsonAutoImports": "on", |
|||
|
|||
"eslint.validate": [ |
|||
"javascript", |
|||
"typescript", |
|||
"javascriptreact", |
|||
"typescriptreact", |
|||
"vue", |
|||
"html", |
|||
"markdown", |
|||
"json", |
|||
"jsonc", |
|||
"json5" |
|||
], |
|||
|
|||
"tailwindCSS.experimental.classRegex": [ |
|||
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"] |
|||
], |
|||
|
|||
"github.copilot.enable": { |
|||
"*": true, |
|||
"markdown": true, |
|||
"plaintext": false, |
|||
"yaml": false |
|||
}, |
|||
|
|||
"cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"], |
|||
|
|||
"i18n-ally.localesPaths": [ |
|||
"packages/locales/src/langs", |
|||
"playground/src/locales/langs", |
|||
"apps/*/src/locales/langs" |
|||
], |
|||
"i18n-ally.pathMatcher": "{locale}/{namespace}.{ext}", |
|||
"i18n-ally.enabledParsers": ["json"], |
|||
"i18n-ally.sourceLanguage": "en", |
|||
"i18n-ally.displayLanguage": "zh-CN", |
|||
"i18n-ally.enabledFrameworks": ["vue", "react"], |
|||
"i18n-ally.keystyle": "nested", |
|||
"i18n-ally.sortKeys": true, |
|||
"i18n-ally.namespace": true, |
|||
|
|||
// 控制相关文件嵌套展示 |
|||
"explorer.fileNesting.enabled": true, |
|||
"explorer.fileNesting.expand": false, |
|||
"explorer.fileNesting.patterns": { |
|||
"*.ts": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx, $(capture).d.ts", |
|||
"*.tsx": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx,$(capture).d.ts", |
|||
"*.env": "$(capture).env.*", |
|||
"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", |
|||
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml", |
|||
"tailwind.config.mjs": "postcss.*" |
|||
}, |
|||
"commentTranslate.hover.enabled": false, |
|||
"commentTranslate.multiLineMerge": true, |
|||
"vue.server.hybridMode": true, |
|||
"typescript.tsdk": "node_modules/typescript/lib", |
|||
"oxc.enable": false, |
|||
"cSpell.words": [ |
|||
"archiver", |
|||
"axios", |
|||
"dotenv", |
|||
"isequal", |
|||
"jspm", |
|||
"napi", |
|||
"nolebase", |
|||
"rollup", |
|||
"vitest" |
|||
] |
|||
} |
|||
@ -1,9 +1,51 @@ |
|||
<script lang="ts" setup> |
|||
import { Fallback } from '@vben/common-ui'; |
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
import { Fallback, VbenButton } from '@vben/common-ui'; |
|||
import { ArrowLeft, LogOut } from '@vben/icons'; |
|||
import { $t } from '@vben/locales'; |
|||
import { preferences } from '@vben/preferences'; |
|||
|
|||
import { Modal } from 'ant-design-vue'; |
|||
|
|||
import { useAuthStore } from '#/store'; |
|||
|
|||
defineOptions({ name: 'Fallback404Demo' }); |
|||
|
|||
const authStore = useAuthStore(); |
|||
const { push } = useRouter(); |
|||
|
|||
// 返回首页 |
|||
function back() { |
|||
push(preferences.app.defaultHomePath); |
|||
} |
|||
|
|||
// 退出登录 |
|||
function logout() { |
|||
Modal.confirm({ |
|||
centered: true, |
|||
title: $t('common.logout'), |
|||
content: $t('ui.widgets.logoutTip'), |
|||
async onOk() { |
|||
await authStore.logout(); |
|||
}, |
|||
}); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Fallback status="404" /> |
|||
<Fallback status="404"> |
|||
<template #action> |
|||
<div class="flex gap-2"> |
|||
<VbenButton size="lg" @click="back"> |
|||
<ArrowLeft class="mr-2 size-4" /> |
|||
{{ $t('common.backToHome') }} |
|||
</VbenButton> |
|||
<VbenButton size="lg" variant="destructive" @click="logout"> |
|||
<LogOut class="mr-2 size-4" /> |
|||
{{ $t('common.logout') }} |
|||
</VbenButton> |
|||
</div> |
|||
</template> |
|||
</Fallback> |
|||
</template> |
|||
|
|||
@ -1,15 +1,98 @@ |
|||
<script lang="ts" setup> |
|||
import { Page } from '@vben/common-ui'; |
|||
import type { BindItem } from '@abp/account'; |
|||
|
|||
import { MySetting } from '@abp/account'; |
|||
import { defineAsyncComponent, ref } from 'vue'; |
|||
|
|||
import { Page, useVbenModal } from '@vben/common-ui'; |
|||
import { useRefresh } from '@vben/hooks'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { MySetting, useExternalLoginsApi } from '@abp/account'; |
|||
import { message, Modal } from 'ant-design-vue'; |
|||
|
|||
defineOptions({ |
|||
name: 'Vben5AccountMySettings', |
|||
}); |
|||
|
|||
const { bindWorkWeixinApi, getExternalLoginsApi, removeExternalLoginApi } = |
|||
useExternalLoginsApi(); |
|||
const { refresh } = useRefresh(); |
|||
|
|||
const [WechatWorkUserBindModal, weComBindModal] = useVbenModal({ |
|||
connectedComponent: defineAsyncComponent(async () => { |
|||
const component = await import('@abp/wechat'); |
|||
return component.WechatWorkUserBinder; |
|||
}), |
|||
}); |
|||
const externalLogins = ref<BindItem[]>([]); |
|||
|
|||
async function onBindWorkWeixin(code: string) { |
|||
weComBindModal.setState({ submitting: true }); |
|||
try { |
|||
await bindWorkWeixinApi({ code }); |
|||
weComBindModal.close(); |
|||
message.success($t('AbpAccount.BindSuccessfully')); |
|||
refresh(); |
|||
} finally { |
|||
weComBindModal.setState({ submitting: false }); |
|||
} |
|||
} |
|||
|
|||
async function onRemoveBind(provider: string, key: string) { |
|||
Modal.confirm({ |
|||
title: $t('AbpUi.AreYouSure'), |
|||
centered: true, |
|||
content: $t('AbpAccount.CancelBindWarningMessage'), |
|||
async onOk() { |
|||
await removeExternalLoginApi({ |
|||
loginProvider: provider, |
|||
providerKey: key, |
|||
}); |
|||
message.success($t('AbpAccount.CancelBindSuccessfully')); |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
function onBind(provider: string) { |
|||
switch (provider.toLocaleLowerCase()) { |
|||
case 'workweixin': { |
|||
weComBindModal.open(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
async function onClick(provider: string, key?: string) { |
|||
if (key) { |
|||
await onRemoveBind(provider, key); |
|||
return; |
|||
} |
|||
onBind(provider); |
|||
} |
|||
|
|||
async function onInit() { |
|||
const res = await getExternalLoginsApi(); |
|||
externalLogins.value = res.externalLogins.map((item) => { |
|||
const userLogin = res.userLogins.find((x) => x.loginProvider === item.name); |
|||
return { |
|||
title: item.displayName, |
|||
description: userLogin?.providerKey ?? $t('AbpAccount.UnBind'), |
|||
buttons: [ |
|||
{ |
|||
title: userLogin?.providerKey |
|||
? $t('AbpAccount.CancelBind') |
|||
: $t('AbpAccount.Bind'), |
|||
type: 'link', |
|||
click: () => onClick(item.name, userLogin?.providerKey), |
|||
}, |
|||
], |
|||
}; |
|||
}); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Page> |
|||
<MySetting /> |
|||
<MySetting :bind-items="externalLogins" @on-bind-init="onInit" /> |
|||
<WechatWorkUserBindModal @on-login="onBindWorkWeixin" /> |
|||
</Page> |
|||
</template> |
|||
|
|||
@ -1,266 +1,32 @@ |
|||
<script lang="ts" setup> |
|||
import type { |
|||
WorkbenchProjectItem, |
|||
WorkbenchQuickNavItem, |
|||
WorkbenchTodoItem, |
|||
WorkbenchTrendItem, |
|||
} from '@vben/common-ui'; |
|||
<script setup lang="ts"> |
|||
import type { FavoriteMenu } from '@abp/platform'; |
|||
|
|||
import { ref } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
import { |
|||
AnalysisChartCard, |
|||
WorkbenchHeader, |
|||
WorkbenchProject, |
|||
WorkbenchQuickNav, |
|||
WorkbenchTodo, |
|||
WorkbenchTrends, |
|||
} from '@vben/common-ui'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { useUserStore } from '@vben/stores'; |
|||
import { openWindow } from '@vben/utils'; |
|||
|
|||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue'; |
|||
|
|||
const userStore = useUserStore(); |
|||
|
|||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整 |
|||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转 |
|||
// 例如:url: /dashboard/workspace |
|||
const projectItems: WorkbenchProjectItem[] = [ |
|||
{ |
|||
color: '', |
|||
content: '不要等待机会,而要创造机会。', |
|||
date: '2021-04-01', |
|||
group: '开源组', |
|||
icon: 'carbon:logo-github', |
|||
title: 'Github', |
|||
url: 'https://github.com', |
|||
}, |
|||
{ |
|||
color: '#3fb27f', |
|||
content: '现在的你决定将来的你。', |
|||
date: '2021-04-01', |
|||
group: '算法组', |
|||
icon: 'ion:logo-vue', |
|||
title: 'Vue', |
|||
url: 'https://vuejs.org', |
|||
}, |
|||
{ |
|||
color: '#e18525', |
|||
content: '没有什么才能比努力更重要。', |
|||
date: '2021-04-01', |
|||
group: '上班摸鱼', |
|||
icon: 'ion:logo-html5', |
|||
title: 'Html5', |
|||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML', |
|||
}, |
|||
{ |
|||
color: '#bf0c2c', |
|||
content: '热情和欲望可以突破一切难关。', |
|||
date: '2021-04-01', |
|||
group: 'UI', |
|||
icon: 'ion:logo-angular', |
|||
title: 'Angular', |
|||
url: 'https://angular.io', |
|||
}, |
|||
{ |
|||
color: '#00d8ff', |
|||
content: '健康的身体是实现目标的基石。', |
|||
date: '2021-04-01', |
|||
group: '技术牛', |
|||
icon: 'bx:bxl-react', |
|||
title: 'React', |
|||
url: 'https://reactjs.org', |
|||
}, |
|||
{ |
|||
color: '#EBD94E', |
|||
content: '路是走出来的,而不是空想出来的。', |
|||
date: '2021-04-01', |
|||
group: '架构组', |
|||
icon: 'ion:logo-javascript', |
|||
title: 'Js', |
|||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript', |
|||
}, |
|||
]; |
|||
|
|||
// 同样,这里的 url 也可以使用以 http 开头的外部链接 |
|||
const quickNavItems: WorkbenchQuickNavItem[] = [ |
|||
{ |
|||
color: '#1fdaca', |
|||
icon: 'ion:home-outline', |
|||
title: '首页', |
|||
url: '/', |
|||
}, |
|||
{ |
|||
color: '#bf0c2c', |
|||
icon: 'ion:grid-outline', |
|||
title: '仪表盘', |
|||
url: '/dashboard', |
|||
}, |
|||
{ |
|||
color: '#e18525', |
|||
icon: 'ion:layers-outline', |
|||
title: '组件', |
|||
url: '/demos/features/icons', |
|||
}, |
|||
{ |
|||
color: '#3fb27f', |
|||
icon: 'ion:settings-outline', |
|||
title: '系统管理', |
|||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整 |
|||
}, |
|||
{ |
|||
color: '#4daf1bc9', |
|||
icon: 'ion:key-outline', |
|||
title: '权限管理', |
|||
url: '/demos/access/page-control', |
|||
}, |
|||
{ |
|||
color: '#00d8ff', |
|||
icon: 'ion:bar-chart-outline', |
|||
title: '图表', |
|||
url: '/analytics', |
|||
}, |
|||
]; |
|||
|
|||
const todoItems = ref<WorkbenchTodoItem[]>([ |
|||
{ |
|||
completed: false, |
|||
content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '审查前端代码提交', |
|||
}, |
|||
{ |
|||
completed: true, |
|||
content: `检查并优化系统性能,降低CPU使用率。`, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '系统性能优化', |
|||
}, |
|||
{ |
|||
completed: false, |
|||
content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '安全检查', |
|||
}, |
|||
{ |
|||
completed: false, |
|||
content: `更新项目中的所有npm依赖包,确保使用最新版本。`, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '更新项目依赖', |
|||
}, |
|||
{ |
|||
completed: false, |
|||
content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '修复UI显示问题', |
|||
}, |
|||
]); |
|||
const trendItems: WorkbenchTrendItem[] = [ |
|||
{ |
|||
avatar: 'svg:avatar-1', |
|||
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`, |
|||
date: '刚刚', |
|||
title: '威廉', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-2', |
|||
content: `关注了 <a>威廉</a> `, |
|||
date: '1个小时前', |
|||
title: '艾文', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-3', |
|||
content: `发布了 <a>个人动态</a> `, |
|||
date: '1天前', |
|||
title: '克里斯', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-4', |
|||
content: `发表文章 <a>如何编写一个Vite插件</a> `, |
|||
date: '2天前', |
|||
title: 'Vben', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-1', |
|||
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`, |
|||
date: '3天前', |
|||
title: '皮特', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-2', |
|||
content: `关闭了问题 <a>如何运行项目</a> `, |
|||
date: '1周前', |
|||
title: '杰克', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-3', |
|||
content: `发布了 <a>个人动态</a> `, |
|||
date: '1周前', |
|||
title: '威廉', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-4', |
|||
content: `推送了代码到 <a>Github</a>`, |
|||
date: '2021-04-01 20:00', |
|||
title: '威廉', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-4', |
|||
content: `发表文章 <a>如何编写使用 Admin Vben</a> `, |
|||
date: '2021-03-01 20:00', |
|||
title: 'Vben', |
|||
}, |
|||
]; |
|||
import { Workbench } from '@abp/platform'; |
|||
|
|||
const router = useRouter(); |
|||
|
|||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整 |
|||
// This is a sample method, adjust according to the actual project requirements |
|||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) { |
|||
if (nav.url?.startsWith('http')) { |
|||
openWindow(nav.url); |
|||
function navTo(menu: FavoriteMenu) { |
|||
if (menu.path?.startsWith('http')) { |
|||
openWindow(menu.path); |
|||
return; |
|||
} |
|||
if (nav.url?.startsWith('/')) { |
|||
router.push(nav.url).catch((error) => { |
|||
if (menu.path?.startsWith('/')) { |
|||
router.push(menu.path).catch((error) => { |
|||
console.error('Navigation failed:', error); |
|||
}); |
|||
} else { |
|||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`); |
|||
console.warn( |
|||
`Unknown URL for navigation item: ${menu.displayName} -> ${menu.path}`, |
|||
); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="p-5"> |
|||
<WorkbenchHeader |
|||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar" |
|||
> |
|||
<template #title> |
|||
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧! |
|||
</template> |
|||
<template #description> 今日晴,20℃ - 32℃! </template> |
|||
</WorkbenchHeader> |
|||
|
|||
<div class="mt-5 flex flex-col lg:flex-row"> |
|||
<div class="mr-4 w-full lg:w-3/5"> |
|||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" /> |
|||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" /> |
|||
</div> |
|||
<div class="w-full lg:w-2/5"> |
|||
<WorkbenchQuickNav |
|||
:items="quickNavItems" |
|||
class="mt-5 lg:mt-0" |
|||
title="快捷导航" |
|||
@click="navTo" |
|||
/> |
|||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" /> |
|||
<AnalysisChartCard class="mt-5" title="访问来源"> |
|||
<AnalyticsVisitsSource /> |
|||
</AnalysisChartCard> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<Workbench @nav-to="navTo" /> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
|
|||
@ -1,7 +1,5 @@ |
|||
export { useAccountApi } from './useAccountApi'; |
|||
export { useExternalLoginsApi } from './useExternalLoginsApi'; |
|||
export { useMySessionApi } from './useMySessionApi'; |
|||
export { usePhoneLoginApi } from './usePhoneLoginApi'; |
|||
export { useProfileApi } from './useProfileApi'; |
|||
export { useQrCodeLoginApi } from './useQrCodeLoginApi'; |
|||
export { useTokenApi } from './useTokenApi'; |
|||
export { useUserInfoApi } from './useUserInfoApi'; |
|||
export { useScanQrCodeApi } from './useScanQrCodeApi'; |
|||
|
|||
@ -0,0 +1,58 @@ |
|||
import type { |
|||
ExternalLoginResultDto, |
|||
RemoveExternalLoginInput, |
|||
WorkWeixinLoginBindInput, |
|||
} from '../types/external-logins'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useExternalLoginsApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 绑定企业微信 |
|||
* @param input 绑定参数 |
|||
* @returns { Promise<void> } |
|||
*/ |
|||
async function bindWorkWeixinApi( |
|||
input: WorkWeixinLoginBindInput, |
|||
): Promise<void> { |
|||
return await request(`/api/account/oauth/work-weixin/bind`, { |
|||
method: 'POST', |
|||
data: input, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 获取外部登录提供者列表 |
|||
* @returns 外部登录提供者列表 |
|||
*/ |
|||
async function getExternalLoginsApi(): Promise<ExternalLoginResultDto> { |
|||
return await request<ExternalLoginResultDto>( |
|||
`/api/account/external-logins`, |
|||
{ |
|||
method: 'GET', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 移除外部登录提供者 |
|||
* @returns { Promise<void> } |
|||
*/ |
|||
async function removeExternalLoginApi( |
|||
input: RemoveExternalLoginInput, |
|||
): Promise<void> { |
|||
return await request(`/api/account/external-logins/remove`, { |
|||
method: 'DELETE', |
|||
params: input, |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
bindWorkWeixinApi, |
|||
getExternalLoginsApi, |
|||
removeExternalLoginApi, |
|||
}; |
|||
} |
|||
@ -1,46 +0,0 @@ |
|||
import type { OAuthTokenResult, PhoneNumberTokenRequest } from '../types/token'; |
|||
|
|||
import { useAppConfig } from '@vben/hooks'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function usePhoneLoginApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 手机验证码登录 |
|||
* @param input 登录参数 |
|||
* @returns 用户token |
|||
*/ |
|||
async function loginApi(input: PhoneNumberTokenRequest) { |
|||
const { audience, clientId, clientSecret } = useAppConfig( |
|||
import.meta.env, |
|||
import.meta.env.PROD, |
|||
); |
|||
const result = await request<OAuthTokenResult>('/connect/token', { |
|||
data: { |
|||
client_id: clientId, |
|||
client_secret: clientSecret, |
|||
grant_type: 'phone_verify', |
|||
phone_number: input.phoneNumber, |
|||
phone_verify_code: input.code, |
|||
scope: audience, |
|||
}, |
|||
headers: { |
|||
'Content-Type': 'application/x-www-form-urlencoded', |
|||
}, |
|||
method: 'POST', |
|||
}); |
|||
return { |
|||
accessToken: result.access_token, |
|||
expiresIn: result.expires_in, |
|||
refreshToken: result.refresh_token, |
|||
tokenType: result.token_type, |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
loginApi, |
|||
}; |
|||
} |
|||
@ -1,73 +0,0 @@ |
|||
import type { |
|||
GenerateQrCodeResult, |
|||
QrCodeUserInfoResult, |
|||
} from '../types/qrcode'; |
|||
import type { OAuthTokenResult, QrCodeTokenRequest } from '../types/token'; |
|||
|
|||
import { useAppConfig } from '@vben/hooks'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useQrCodeLoginApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 生成登录二维码 |
|||
* @returns 二维码信息 |
|||
*/ |
|||
function generateApi(): Promise<GenerateQrCodeResult> { |
|||
return request<GenerateQrCodeResult>('/api/account/qrcode/generate', { |
|||
method: 'POST', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 检查二维码状态 |
|||
* @param key 二维码Key |
|||
* @returns 二维码信息 |
|||
*/ |
|||
function checkCodeApi(key: string): Promise<QrCodeUserInfoResult> { |
|||
return request<QrCodeUserInfoResult>(`/api/account/qrcode/${key}/check`, { |
|||
method: 'GET', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 二维码登录 |
|||
* @param input 登录参数 |
|||
* @returns 用户token |
|||
*/ |
|||
async function loginApi(input: QrCodeTokenRequest) { |
|||
const { audience, clientId, clientSecret } = useAppConfig( |
|||
import.meta.env, |
|||
import.meta.env.PROD, |
|||
); |
|||
const result = await request<OAuthTokenResult>('/connect/token', { |
|||
data: { |
|||
client_id: clientId, |
|||
client_secret: clientSecret, |
|||
grant_type: 'qr_code', |
|||
qrcode_key: input.key, |
|||
scope: audience, |
|||
tenant_id: input.tenantId, |
|||
}, |
|||
headers: { |
|||
'Content-Type': 'application/x-www-form-urlencoded', |
|||
}, |
|||
method: 'POST', |
|||
}); |
|||
return { |
|||
accessToken: result.access_token, |
|||
expiresIn: result.expires_in, |
|||
refreshToken: result.refresh_token, |
|||
tokenType: result.token_type, |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
checkCodeApi, |
|||
generateApi, |
|||
loginApi, |
|||
}; |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
import type { |
|||
GenerateQrCodeResult, |
|||
QrCodeUserInfoResult, |
|||
} from '../types/qrcode'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useScanQrCodeApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 生成登录二维码 |
|||
* @returns 二维码信息 |
|||
*/ |
|||
function generateApi(): Promise<GenerateQrCodeResult> { |
|||
return request<GenerateQrCodeResult>('/api/account/qrcode/generate', { |
|||
method: 'POST', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 检查二维码状态 |
|||
* @param key 二维码Key |
|||
* @returns 二维码信息 |
|||
*/ |
|||
function checkCodeApi(key: string): Promise<QrCodeUserInfoResult> { |
|||
return request<QrCodeUserInfoResult>(`/api/account/qrcode/${key}/check`, { |
|||
method: 'GET', |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
checkCodeApi, |
|||
generateApi, |
|||
}; |
|||
} |
|||
@ -1,83 +0,0 @@ |
|||
import type { |
|||
OAuthTokenRefreshModel, |
|||
OAuthTokenResult, |
|||
PasswordTokenRequestModel, |
|||
TokenResult, |
|||
} from '../types'; |
|||
|
|||
import { useAppConfig } from '@vben/hooks'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useTokenApi() { |
|||
const { cancel, request } = useRequest(); |
|||
/** |
|||
* 用户登录 |
|||
* @param input 参数 |
|||
* @returns 用户token |
|||
*/ |
|||
async function loginApi( |
|||
input: PasswordTokenRequestModel, |
|||
): Promise<TokenResult> { |
|||
const { audience, clientId, clientSecret } = useAppConfig( |
|||
import.meta.env, |
|||
import.meta.env.PROD, |
|||
); |
|||
const result = await request<OAuthTokenResult>('/connect/token', { |
|||
data: { |
|||
client_id: clientId, |
|||
client_secret: clientSecret, |
|||
grant_type: 'password', |
|||
scope: audience, |
|||
...input, |
|||
}, |
|||
headers: { |
|||
'Content-Type': 'application/x-www-form-urlencoded', |
|||
}, |
|||
method: 'POST', |
|||
}); |
|||
return { |
|||
accessToken: result.access_token, |
|||
expiresIn: result.expires_in, |
|||
refreshToken: result.refresh_token, |
|||
tokenType: result.token_type, |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* 刷新令牌 |
|||
* @param input 参数 |
|||
* @returns 用户token |
|||
*/ |
|||
async function refreshTokenApi(input: OAuthTokenRefreshModel) { |
|||
const { audience, clientId, clientSecret } = useAppConfig( |
|||
import.meta.env, |
|||
import.meta.env.PROD, |
|||
); |
|||
const result = await request<OAuthTokenResult>('/connect/token', { |
|||
data: { |
|||
client_id: clientId, |
|||
client_secret: clientSecret, |
|||
grant_type: 'refresh_token', |
|||
refresh_token: input.refreshToken, |
|||
scope: audience, |
|||
}, |
|||
headers: { |
|||
'Content-Type': 'application/x-www-form-urlencoded', |
|||
}, |
|||
method: 'POST', |
|||
}); |
|||
return { |
|||
accessToken: result.access_token, |
|||
expiresIn: result.expires_in, |
|||
refreshToken: result.refresh_token, |
|||
tokenType: result.token_type, |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
loginApi, |
|||
refreshTokenApi, |
|||
}; |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
import type { OAuthUserInfo, UserInfo } from '../types/user'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useUserInfoApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 获取用户信息 |
|||
*/ |
|||
async function getUserInfoApi(): Promise<UserInfo> { |
|||
const result = await request<OAuthUserInfo>('/connect/userinfo', { |
|||
method: 'GET', |
|||
}); |
|||
return { |
|||
...result, |
|||
emailVerified: result.email_verified, |
|||
givenName: result.given_name, |
|||
phoneNumberVerified: result.phone_number_verified, |
|||
preferredUsername: result.preferred_username, |
|||
uniqueName: result.unique_name, |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
getUserInfoApi, |
|||
}; |
|||
} |
|||
@ -1,2 +1,2 @@ |
|||
export * from './useOAuthError'; |
|||
export * from './useOidcClient'; |
|||
export * from './useOAuthService'; |
|||
|
|||
@ -0,0 +1,17 @@ |
|||
import type { ButtonType } from 'ant-design-vue/lib/button'; |
|||
|
|||
interface BindButton { |
|||
click: () => Promise<void> | void; |
|||
title: string; |
|||
type?: ButtonType; |
|||
} |
|||
|
|||
interface BindItem { |
|||
buttons?: BindButton[]; |
|||
description?: string; |
|||
enable?: boolean; |
|||
slot?: string; |
|||
title: string; |
|||
} |
|||
|
|||
export type { BindItem }; |
|||
@ -0,0 +1,32 @@ |
|||
interface UserLoginInfoDto { |
|||
loginProvider: string; |
|||
providerDisplayName: string; |
|||
providerKey: string; |
|||
} |
|||
|
|||
interface ExternalLoginInfoDto { |
|||
displayName: string; |
|||
name: string; |
|||
} |
|||
|
|||
interface WorkWeixinLoginBindInput { |
|||
code: string; |
|||
} |
|||
|
|||
interface ExternalLoginResultDto { |
|||
externalLogins: ExternalLoginInfoDto[]; |
|||
userLogins: UserLoginInfoDto[]; |
|||
} |
|||
|
|||
interface RemoveExternalLoginInput { |
|||
loginProvider: string; |
|||
providerKey: string; |
|||
} |
|||
|
|||
export type { |
|||
ExternalLoginInfoDto, |
|||
ExternalLoginResultDto, |
|||
RemoveExternalLoginInput, |
|||
UserLoginInfoDto, |
|||
WorkWeixinLoginBindInput, |
|||
}; |
|||
@ -1,4 +1,6 @@ |
|||
export * from './account'; |
|||
export * from './bind'; |
|||
export * from './external-logins'; |
|||
export * from './profile'; |
|||
export * from './token'; |
|||
export * from './user'; |
|||
|
|||
@ -0,0 +1,42 @@ |
|||
const hexList: string[] = []; |
|||
for (let i = 0; i <= 15; i++) { |
|||
hexList[i] = i.toString(16); |
|||
} |
|||
|
|||
export function buildUUID(): string { |
|||
let uuid = ''; |
|||
for (let i = 1; i <= 36; i++) { |
|||
switch (i) { |
|||
case 9: |
|||
case 14: |
|||
case 19: |
|||
case 24: { |
|||
uuid += '-'; |
|||
|
|||
break; |
|||
} |
|||
case 15: { |
|||
uuid += 4; |
|||
|
|||
break; |
|||
} |
|||
case 20: { |
|||
uuid += hexList[(Math.random() * 4) | 8]; |
|||
|
|||
break; |
|||
} |
|||
default: { |
|||
uuid += hexList[Math.trunc(Math.random() * 16)]; |
|||
} |
|||
} |
|||
} |
|||
return uuid.replaceAll('-', ''); |
|||
} |
|||
|
|||
let unique = 0; |
|||
export function buildShortUUID(prefix = ''): string { |
|||
const time = Date.now(); |
|||
const random = Math.floor(Math.random() * 1_000_000_000); |
|||
unique++; |
|||
return `${prefix}_${random}${unique}${String(time)}`; |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
import type { ListResultDto } from '@abp/core'; |
|||
|
|||
import type { |
|||
UserFavoriteMenuCreateDto, |
|||
UserFavoriteMenuDto, |
|||
} from '../types/favorites'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useMyFavoriteMenusApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 新增常用菜单 |
|||
* @param input 参数 |
|||
* @returns 常用菜单 |
|||
*/ |
|||
function createApi( |
|||
input: UserFavoriteMenuCreateDto, |
|||
): Promise<UserFavoriteMenuDto> { |
|||
return request<UserFavoriteMenuDto>( |
|||
`/api/platform/menus/favorites/my-favorite-menus`, |
|||
{ |
|||
data: input, |
|||
method: 'POST', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 删除常用菜单 |
|||
* @param id 菜单Id |
|||
*/ |
|||
function deleteApi(id: string): Promise<void> { |
|||
return request(`/api/platform/menus/favorites/my-favorite-menus/${id}`, { |
|||
method: 'DELETE', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 获取常用菜单列表 |
|||
* @param framework ui框架 |
|||
* @returns 菜单列表 |
|||
*/ |
|||
function getListApi( |
|||
framework?: string, |
|||
): Promise<ListResultDto<UserFavoriteMenuDto>> { |
|||
return request<ListResultDto<UserFavoriteMenuDto>>( |
|||
`/api/platform/menus/favorites/my-favorite-menus?framework=${framework}`, |
|||
{ |
|||
method: 'GET', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
createApi, |
|||
deleteApi, |
|||
getListApi, |
|||
}; |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
<script lang="ts" setup> |
|||
import { VbenAvatar } from '@vben-core/shadcn-ui'; |
|||
|
|||
interface Props { |
|||
avatar?: string; |
|||
notifierCount?: number; |
|||
text?: string; |
|||
} |
|||
|
|||
defineOptions({ |
|||
name: 'WorkbenchHeader', |
|||
}); |
|||
|
|||
withDefaults(defineProps<Props>(), { |
|||
avatar: '', |
|||
text: '', |
|||
notifierCount: 0, |
|||
}); |
|||
</script> |
|||
<template> |
|||
<div class="card-box p-4 py-6 lg:flex"> |
|||
<VbenAvatar :alt="text" :src="avatar" class="size-20" /> |
|||
<div |
|||
v-if="$slots.title || $slots.description" |
|||
class="flex flex-col justify-center md:ml-6 md:mt-0" |
|||
> |
|||
<h1 v-if="$slots.title" class="text-md font-semibold md:text-xl"> |
|||
<slot name="title"></slot> |
|||
</h1> |
|||
<span v-if="$slots.description" class="text-foreground/80 mt-1"> |
|||
<slot name="description"></slot> |
|||
</span> |
|||
</div> |
|||
<div class="mt-4 flex flex-1 justify-end md:mt-0"> |
|||
<div class="flex flex-col justify-center text-right"> |
|||
<span class="text-foreground/80"> |
|||
{{ $t('workbench.header.notifier.title') }} |
|||
</span> |
|||
<a class="text-2xl">{{ |
|||
$t('workbench.header.notifier.count', [notifierCount]) |
|||
}}</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
@ -0,0 +1,124 @@ |
|||
<script setup lang="ts"> |
|||
import type { FavoriteMenu } from '../types'; |
|||
|
|||
import { computed, h } from 'vue'; |
|||
|
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { |
|||
Card, |
|||
CardContent, |
|||
CardHeader, |
|||
CardTitle, |
|||
VbenIcon, |
|||
} from '@vben-core/shadcn-ui'; |
|||
|
|||
import { DeleteOutlined } from '@ant-design/icons-vue'; |
|||
import { Dropdown, Menu, Modal } from 'ant-design-vue'; |
|||
|
|||
interface Props { |
|||
items?: FavoriteMenu[]; |
|||
title: string; |
|||
} |
|||
|
|||
defineOptions({ |
|||
name: 'WorkbenchQuickNav', |
|||
}); |
|||
|
|||
const props = withDefaults(defineProps<Props>(), { |
|||
items: () => [], |
|||
}); |
|||
|
|||
const emits = defineEmits<{ |
|||
(event: 'click', menu: FavoriteMenu): void; |
|||
(event: 'delete', menu: FavoriteMenu): void; |
|||
(event: 'add'): void; |
|||
}>(); |
|||
|
|||
const MenuItem = Menu.Item; |
|||
|
|||
const getFavoriteMenus = computed(() => { |
|||
const addMenu: FavoriteMenu = { |
|||
id: 'addMenu', |
|||
displayName: $t('workbench.content.favoriteMenu.create'), |
|||
icon: 'ion:add-outline', |
|||
color: '#00bfff', |
|||
isDefault: true, |
|||
}; |
|||
return [...props.items, addMenu]; |
|||
}); |
|||
|
|||
function onClick(menu: FavoriteMenu) { |
|||
if (menu.id === 'addMenu') { |
|||
emits('add'); |
|||
return; |
|||
} |
|||
emits('click', menu); |
|||
} |
|||
|
|||
function onMenuClick(key: string, menu: FavoriteMenu) { |
|||
switch (key) { |
|||
case 'delete': { |
|||
Modal.confirm({ |
|||
centered: true, |
|||
iconType: 'warning', |
|||
title: $t('AbpUi.AreYouSure'), |
|||
content: $t('AbpUi.ItemWillBeDeletedMessage'), |
|||
okCancel: true, |
|||
onOk: () => { |
|||
emits('delete', menu); |
|||
}, |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Card> |
|||
<CardHeader class="py-4"> |
|||
<CardTitle class="text-lg">{{ title }}</CardTitle> |
|||
</CardHeader> |
|||
<CardContent class="flex flex-wrap p-0"> |
|||
<template |
|||
v-for="(item, index) in getFavoriteMenus" |
|||
:key="item.displayName" |
|||
> |
|||
<Dropdown :trigger="['contextmenu']"> |
|||
<div |
|||
:class="{ |
|||
'border-r-0': index % 3 === 2, |
|||
'border-b-0': index < 3, |
|||
'pb-4': index > 2, |
|||
'rounded-bl-xl': index === items.length - 3, |
|||
'rounded-br-xl': index === items.length - 1, |
|||
}" |
|||
class="flex-col-center border-border group w-1/3 cursor-pointer border-r border-t py-8 hover:shadow-xl" |
|||
@click="onClick(item)" |
|||
> |
|||
<VbenIcon |
|||
:color="item.color" |
|||
:icon="item.icon" |
|||
class="size-7 transition-all duration-300 group-hover:scale-125" |
|||
/> |
|||
<span class="text-md mt-2 truncate">{{ item.displayName }}</span> |
|||
</div> |
|||
<template #overlay> |
|||
<Menu |
|||
v-if="!item.isDefault" |
|||
@click=" |
|||
({ key: menuKey }) => onMenuClick(menuKey.toString(), item) |
|||
" |
|||
> |
|||
<MenuItem key="delete" :icon="h(DeleteOutlined)"> |
|||
{{ $t('workbench.content.favoriteMenu.delete') }} |
|||
</MenuItem> |
|||
</Menu> |
|||
</template> |
|||
</Dropdown> |
|||
</template> |
|||
</CardContent> |
|||
</Card> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,137 @@ |
|||
<script setup lang="ts"> |
|||
import type { MenuDto, UserFavoriteMenuDto } from '../../../types'; |
|||
|
|||
import { defineAsyncComponent, ref } from 'vue'; |
|||
|
|||
import { useVbenForm, useVbenModal } from '@vben/common-ui'; |
|||
import { useAppConfig } from '@vben/hooks'; |
|||
import { IconifyIcon } from '@vben/icons'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { listToTree } from '@abp/core'; |
|||
import { message, TreeSelect } from 'ant-design-vue'; |
|||
|
|||
import { useMyFavoriteMenusApi } from '../../../api/useMyFavoriteMenusApi'; |
|||
import { useMyMenusApi } from '../../../api/useMyMenusApi'; |
|||
|
|||
const emits = defineEmits<{ |
|||
(event: 'change', data: UserFavoriteMenuDto): void; |
|||
}>(); |
|||
|
|||
const ColorPicker = defineAsyncComponent(() => |
|||
import('vue3-colorpicker').then((res) => { |
|||
import('vue3-colorpicker/style.css'); |
|||
return res.ColorPicker; |
|||
}), |
|||
); |
|||
|
|||
const availableMenus = ref<MenuDto[]>([]); |
|||
|
|||
const { getAllApi } = useMyMenusApi(); |
|||
const { createApi } = useMyFavoriteMenusApi(); |
|||
const { uiFramework } = useAppConfig(import.meta.env, import.meta.env.PROD); |
|||
|
|||
const [Form, formApi] = useVbenForm({ |
|||
schema: [ |
|||
{ |
|||
label: $t('workbench.content.favoriteMenu.select'), |
|||
fieldName: 'menuId', |
|||
component: 'TreeSelect', |
|||
rules: 'selectRequired', |
|||
}, |
|||
{ |
|||
label: $t('workbench.content.favoriteMenu.color'), |
|||
fieldName: 'color', |
|||
component: 'ColorPicker', |
|||
defaultValue: '#000000', |
|||
modelPropName: 'pureColor', |
|||
}, |
|||
{ |
|||
label: $t('workbench.content.favoriteMenu.alias'), |
|||
fieldName: 'aliasName', |
|||
component: 'Input', |
|||
}, |
|||
{ |
|||
label: $t('workbench.content.favoriteMenu.icon'), |
|||
fieldName: 'icon', |
|||
component: 'IconPicker', |
|||
}, |
|||
], |
|||
showDefaultActions: false, |
|||
handleSubmit: onSubmit, |
|||
commonConfig: { |
|||
colon: true, |
|||
componentProps: { |
|||
class: 'w-full', |
|||
}, |
|||
}, |
|||
}); |
|||
const [Modal, modalApi] = useVbenModal({ |
|||
async onConfirm() { |
|||
await formApi.validateAndSubmitForm(); |
|||
}, |
|||
async onOpenChange(isOpen) { |
|||
if (isOpen) { |
|||
await onInitMenus(); |
|||
} |
|||
}, |
|||
}); |
|||
|
|||
async function onInitMenus() { |
|||
const { items } = await getAllApi({ |
|||
framework: uiFramework, |
|||
}); |
|||
const menus = listToTree<MenuDto>(items, { id: 'id', pid: 'parentId' }); |
|||
availableMenus.value = menus; |
|||
} |
|||
|
|||
async function onSubmit(values: Record<string, any>) { |
|||
try { |
|||
modalApi.setState({ submitting: true }); |
|||
const menuDto = await createApi({ |
|||
framework: uiFramework, |
|||
menuId: values.menuId, |
|||
color: values.color, |
|||
icon: values.icon, |
|||
aliasName: values.aliasName, |
|||
}); |
|||
message.success($t('AbpUi.SavedSuccessfully')); |
|||
emits('change', menuDto); |
|||
modalApi.close(); |
|||
} finally { |
|||
modalApi.setState({ submitting: false }); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Modal :title="$t('workbench.content.favoriteMenu.manage')"> |
|||
<Form> |
|||
<template #color="slotProps"> |
|||
<div class="flex flex-row items-center"> |
|||
<ColorPicker v-bind="slotProps" format="hex" /> |
|||
<span>({{ slotProps.value }})</span> |
|||
</div> |
|||
</template> |
|||
<template #menuId="slotProps"> |
|||
<TreeSelect |
|||
allow-clear |
|||
class="w-full" |
|||
tree-icon |
|||
v-bind="slotProps" |
|||
:field-names="{ label: 'displayName', value: 'id' }" |
|||
:tree-data="availableMenus" |
|||
> |
|||
<template #title="item"> |
|||
<div class="flex flex-row items-center gap-1"> |
|||
<IconifyIcon v-if="item.meta?.icon" :icon="item.meta.icon" /> |
|||
<span>{{ item.displayName }}</span> |
|||
</div> |
|||
</template> |
|||
</TreeSelect> |
|||
</template> |
|||
</Form> |
|||
</Modal> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,64 @@ |
|||
<script setup lang="ts"> |
|||
import type { WorkbenchTodoItem } from '@vben/common-ui'; |
|||
|
|||
import { |
|||
Card, |
|||
CardContent, |
|||
CardHeader, |
|||
CardTitle, |
|||
VbenCheckbox, |
|||
} from '@vben-core/shadcn-ui'; |
|||
|
|||
interface Props { |
|||
items?: WorkbenchTodoItem[]; |
|||
title: string; |
|||
} |
|||
|
|||
defineOptions({ |
|||
name: 'WorkbenchTodo', |
|||
}); |
|||
|
|||
withDefaults(defineProps<Props>(), { |
|||
items: () => [], |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<Card> |
|||
<CardHeader class="py-4"> |
|||
<CardTitle class="text-lg">{{ title }}</CardTitle> |
|||
</CardHeader> |
|||
<slot v-if="items.length === 0" name="empty"></slot> |
|||
<CardContent v-else class="flex flex-wrap p-5 pt-0"> |
|||
<ul class="divide-border w-full divide-y" role="list"> |
|||
<li |
|||
v-for="item in items" |
|||
:key="item.title" |
|||
:class="{ |
|||
'select-none line-through opacity-60': item.completed, |
|||
}" |
|||
class="flex cursor-pointer justify-between gap-x-6 py-5" |
|||
> |
|||
<div class="flex min-w-0 items-center gap-x-4"> |
|||
<VbenCheckbox v-model:checked="item.completed" name="completed" /> |
|||
<div class="min-w-0 flex-auto"> |
|||
<p class="text-foreground text-sm font-semibold leading-6"> |
|||
{{ item.title }} |
|||
</p> |
|||
<!-- eslint-disable vue/no-v-html --> |
|||
<p |
|||
class="text-foreground/80 *:text-primary mt-1 truncate text-xs leading-5" |
|||
v-html="item.content" |
|||
></p> |
|||
</div> |
|||
</div> |
|||
<div class="hidden h-full shrink-0 sm:flex sm:flex-col sm:items-end"> |
|||
<span class="text-foreground/80 mt-6 text-xs leading-6"> |
|||
{{ item.date }} |
|||
</span> |
|||
</div> |
|||
</li> |
|||
</ul> |
|||
</CardContent> |
|||
</Card> |
|||
</template> |
|||
@ -0,0 +1,65 @@ |
|||
<script setup lang="ts"> |
|||
import type { WorkbenchTrendItem } from '@vben/common-ui'; |
|||
|
|||
import { |
|||
Card, |
|||
CardContent, |
|||
CardHeader, |
|||
CardTitle, |
|||
VbenIcon, |
|||
} from '@vben-core/shadcn-ui'; |
|||
|
|||
interface Props { |
|||
items?: WorkbenchTrendItem[]; |
|||
title: string; |
|||
} |
|||
|
|||
defineOptions({ |
|||
name: 'WorkbenchTrends', |
|||
}); |
|||
|
|||
withDefaults(defineProps<Props>(), { |
|||
items: () => [], |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<Card> |
|||
<CardHeader class="py-4"> |
|||
<CardTitle class="text-lg">{{ title }}</CardTitle> |
|||
</CardHeader> |
|||
<slot v-if="items.length === 0" name="empty"></slot> |
|||
<CardContent v-else class="flex flex-wrap p-5 pt-0"> |
|||
<ul class="divide-border w-full divide-y" role="list"> |
|||
<li |
|||
v-for="item in items" |
|||
:key="item.title" |
|||
class="flex justify-between gap-x-6 py-5" |
|||
> |
|||
<div class="flex min-w-0 items-center gap-x-4"> |
|||
<VbenIcon |
|||
:icon="item.avatar" |
|||
alt="" |
|||
class="size-10 flex-none rounded-full" |
|||
/> |
|||
<div class="min-w-0 flex-auto"> |
|||
<p class="text-foreground text-sm font-semibold leading-6"> |
|||
{{ item.title }} |
|||
</p> |
|||
<!-- eslint-disable vue/no-v-html --> |
|||
<p |
|||
class="text-foreground/80 *:text-primary mt-1 truncate text-xs leading-5" |
|||
v-html="item.content" |
|||
></p> |
|||
</div> |
|||
</div> |
|||
<div class="hidden h-full shrink-0 sm:flex sm:flex-col sm:items-end"> |
|||
<span class="text-foreground/80 mt-6 text-xs leading-6"> |
|||
{{ item.date }} |
|||
</span> |
|||
</div> |
|||
</li> |
|||
</ul> |
|||
</CardContent> |
|||
</Card> |
|||
</template> |
|||
@ -0,0 +1,218 @@ |
|||
<script setup lang="ts"> |
|||
import type { WorkbenchTodoItem, WorkbenchTrendItem } from '@vben/common-ui'; |
|||
|
|||
import type { FavoriteMenu } from './types'; |
|||
|
|||
import { computed, defineAsyncComponent, onMounted, ref } from 'vue'; |
|||
|
|||
import { useVbenModal } from '@vben/common-ui'; |
|||
import { useAppConfig } from '@vben/hooks'; |
|||
import { $t } from '@vben/locales'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { useUserStore } from '@vben/stores'; |
|||
|
|||
import { formatToDateTime } from '@abp/core'; |
|||
import { |
|||
NotificationReadState, |
|||
useMyNotifilersApi, |
|||
useNotificationSerializer, |
|||
} from '@abp/notifications'; |
|||
import { Empty, message } from 'ant-design-vue'; |
|||
|
|||
import { useMyFavoriteMenusApi } from '../../api/useMyFavoriteMenusApi'; |
|||
import WorkbenchHeader from './components/WorkbenchHeader.vue'; |
|||
import WorkbenchQuickNav from './components/WorkbenchQuickNav.vue'; |
|||
import WorkbenchTodo from './components/WorkbenchTodo.vue'; |
|||
import WorkbenchTrends from './components/WorkbenchTrends.vue'; |
|||
|
|||
defineEmits<{ |
|||
(event: 'navTo', menu: FavoriteMenu): void; |
|||
}>(); |
|||
|
|||
const userStore = useUserStore(); |
|||
const { getMyNotifilersApi } = useMyNotifilersApi(); |
|||
const { getListApi: getFavoriteMenusApi, deleteApi: deleteFavoriteMenuApi } = |
|||
useMyFavoriteMenusApi(); |
|||
const { deserialize } = useNotificationSerializer(); |
|||
const { uiFramework } = useAppConfig(import.meta.env, import.meta.env.PROD); |
|||
|
|||
const defaultMenus: FavoriteMenu[] = [ |
|||
{ |
|||
id: '1', |
|||
color: '#1fdaca', |
|||
icon: 'ion:home-outline', |
|||
displayName: $t('workbench.content.favoriteMenu.home'), |
|||
path: '/', |
|||
isDefault: true, |
|||
}, |
|||
{ |
|||
id: '2', |
|||
color: '#bf0c2c', |
|||
icon: 'ion:grid-outline', |
|||
displayName: $t('workbench.content.favoriteMenu.dashboard'), |
|||
path: '/', |
|||
isDefault: true, |
|||
}, |
|||
{ |
|||
id: '3', |
|||
color: '#00d8ff', |
|||
icon: 'ant-design:notification-outlined', |
|||
displayName: $t('workbench.content.favoriteMenu.notifiers'), |
|||
path: '/manage/notifications/my-notifilers', |
|||
isDefault: true, |
|||
}, |
|||
{ |
|||
id: '4', |
|||
color: '#4daf1bc9', |
|||
icon: 'tdesign:user-setting', |
|||
displayName: $t('workbench.content.favoriteMenu.settings'), |
|||
path: '/account/my-settings', |
|||
isDefault: true, |
|||
}, |
|||
{ |
|||
id: '5', |
|||
color: '#3fb27f', |
|||
icon: 'hugeicons:profile-02', |
|||
displayName: $t('workbench.content.favoriteMenu.profile'), |
|||
path: '/account/profile', |
|||
isDefault: true, |
|||
}, |
|||
]; |
|||
const unReadNotifilerCount = ref(0); |
|||
const unReadNotifilers = ref<WorkbenchTrendItem[]>([]); |
|||
const favoriteMenus = ref<FavoriteMenu[]>([]); |
|||
const todoList = ref<WorkbenchTodoItem[]>([]); |
|||
|
|||
const getFavoriteMenus = computed(() => { |
|||
return [...defaultMenus, ...favoriteMenus.value]; |
|||
}); |
|||
const getWelcomeTitle = computed(() => { |
|||
const now = new Date(); |
|||
const hour = now.getHours(); |
|||
if (hour < 12) { |
|||
return $t('workbench.header.welcome.morning', [ |
|||
userStore.userInfo?.realName, |
|||
]); |
|||
} |
|||
if (hour < 14) { |
|||
return $t('workbench.header.welcome.atoon', [userStore.userInfo?.realName]); |
|||
} |
|||
if (hour < 17) { |
|||
return $t('workbench.header.welcome.afternoon', [ |
|||
userStore.userInfo?.realName, |
|||
]); |
|||
} |
|||
if (hour < 24) { |
|||
return $t('workbench.header.welcome.evening', [ |
|||
userStore.userInfo?.realName, |
|||
]); |
|||
} |
|||
return ''; |
|||
}); |
|||
|
|||
const [WorkbenchQuickNavModal, quickNavModalApi] = useVbenModal({ |
|||
connectedComponent: defineAsyncComponent( |
|||
() => import('./components/WorkbenchQuickNavModal.vue'), |
|||
), |
|||
}); |
|||
|
|||
async function onInit() { |
|||
await Promise.all([ |
|||
onInitFavoriteMenus(), |
|||
onInitNotifiers(), |
|||
onInitTodoList(), |
|||
]); |
|||
} |
|||
async function onInitFavoriteMenus() { |
|||
const { items } = await getFavoriteMenusApi(uiFramework); |
|||
favoriteMenus.value = items.map((item) => { |
|||
return { |
|||
...item, |
|||
id: item.menuId, |
|||
isDefault: false, |
|||
}; |
|||
}); |
|||
} |
|||
async function onInitNotifiers() { |
|||
const { items, totalCount } = await getMyNotifilersApi({ |
|||
maxResultCount: 10, |
|||
readState: NotificationReadState.UnRead, |
|||
}); |
|||
unReadNotifilers.value = items.map((item) => { |
|||
const notifier = deserialize(item); |
|||
return { |
|||
avatar: '', |
|||
date: formatToDateTime(item.creationTime), |
|||
title: notifier.title, |
|||
content: notifier.message, |
|||
}; |
|||
}); |
|||
unReadNotifilerCount.value = totalCount; |
|||
} |
|||
async function onInitTodoList() { |
|||
// TODO: 实现待办事项列表 |
|||
todoList.value = []; |
|||
} |
|||
|
|||
function onCreatingFavoriteMenu() { |
|||
quickNavModalApi.open(); |
|||
} |
|||
|
|||
async function onDeleteFavoriteMenu(menu: FavoriteMenu) { |
|||
await deleteFavoriteMenuApi(menu.id); |
|||
await onInitFavoriteMenus(); |
|||
message.success($t('AbpUi.SuccessfullyDeleted')); |
|||
} |
|||
|
|||
onMounted(onInit); |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="p-5"> |
|||
<WorkbenchHeader |
|||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar" |
|||
:text="userStore.userInfo?.realName" |
|||
:notifier-count="unReadNotifilerCount" |
|||
> |
|||
<template #title> |
|||
{{ getWelcomeTitle }} |
|||
</template> |
|||
<template #description> 今日晴,20℃ - 32℃! </template> |
|||
</WorkbenchHeader> |
|||
|
|||
<div class="mt-5 flex flex-col lg:flex-row"> |
|||
<div class="mr-4 w-full lg:w-3/5"> |
|||
<WorkbenchQuickNav |
|||
:items="getFavoriteMenus" |
|||
class="mt-5 lg:mt-0" |
|||
:title="$t('workbench.content.favoriteMenu.title')" |
|||
@add="onCreatingFavoriteMenu" |
|||
@delete="onDeleteFavoriteMenu" |
|||
@click="(menu: FavoriteMenu) => $emit('navTo', menu)" |
|||
/> |
|||
<WorkbenchTodo |
|||
:items="todoList" |
|||
class="mt-5" |
|||
:title="$t('workbench.content.todo.title')" |
|||
> |
|||
<template #empty> |
|||
<Empty /> |
|||
</template> |
|||
</WorkbenchTodo> |
|||
</div> |
|||
<div class="w-full lg:w-2/5"> |
|||
<WorkbenchTrends |
|||
:items="unReadNotifilers" |
|||
:title="$t('workbench.content.trends.title')" |
|||
> |
|||
<template #empty> |
|||
<Empty /> |
|||
</template> |
|||
</WorkbenchTrends> |
|||
</div> |
|||
</div> |
|||
<WorkbenchQuickNavModal @change="onInitFavoriteMenus" /> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,10 @@ |
|||
interface FavoriteMenu { |
|||
color?: string; |
|||
displayName: string; |
|||
icon?: string; |
|||
id: string; |
|||
isDefault: boolean; |
|||
path?: string; |
|||
} |
|||
|
|||
export type { FavoriteMenu }; |
|||
@ -1,4 +1,5 @@ |
|||
export * from './api'; |
|||
export * from './components'; |
|||
export * from './hooks'; |
|||
export * from './locales'; |
|||
export * from './types'; |
|||
|
|||
@ -0,0 +1,20 @@ |
|||
import type { SupportedLanguagesType } from '@vben/locales'; |
|||
|
|||
import { loadLocalesMapFromDir } from '@vben/locales'; |
|||
|
|||
const modules = import.meta.glob('./langs/**/*.json'); |
|||
|
|||
const localesMap = loadLocalesMapFromDir( |
|||
/\.\/langs\/([^/]+)\/(.*)\.json$/, |
|||
modules, |
|||
); |
|||
|
|||
/** |
|||
* 加载平台服务本地化资源 |
|||
* @param lang 当前语言 |
|||
* @returns 资源集合 |
|||
*/ |
|||
export async function loadPaltformMessages(lang: SupportedLanguagesType) { |
|||
const locales = localesMap[lang]?.(); |
|||
return locales; |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
{ |
|||
"header": { |
|||
"welcome": { |
|||
"atoon": "Good afternoon, {0}, pay attention to rest oh~", |
|||
"afternoon": "Good afternoon, {0}, relax in time, can improve work efficiency~", |
|||
"evening": "Good evening, {0}. Still at work? The off work~", |
|||
"morning": "Good morning, {0}. Begin your day~" |
|||
}, |
|||
"notifier": { |
|||
"title": "Notifier", |
|||
"count": "({0})" |
|||
} |
|||
}, |
|||
"content": { |
|||
"favoriteMenu": { |
|||
"title": "Favorite Menus", |
|||
"home": "Home", |
|||
"dashboard": "Dashboard", |
|||
"profile": "Personal Profile", |
|||
"settings": "Personal Settings", |
|||
"notifiers": "Notifiers", |
|||
"manage": "Manage menu", |
|||
"create": "New menu", |
|||
"delete": "Delete Menu", |
|||
"select": "Select Menu", |
|||
"color": "Select Color", |
|||
"alias": "Alias Name", |
|||
"icon": "Icon" |
|||
}, |
|||
"trends": { |
|||
"title": "Latest News" |
|||
}, |
|||
"todo": { |
|||
"title": "Todo List" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
{ |
|||
"header": { |
|||
"welcome": { |
|||
"atoon": "中午好, {0}, 注意休息哦~", |
|||
"afternoon": "下午好, {0}, 适时放松,可以提高工作效率~", |
|||
"evening": "晚上好, {0}, 还在工作么?该下班了~", |
|||
"morning": "早安, {0}, 开始您一天的工作吧~" |
|||
}, |
|||
"notifier": { |
|||
"title": "通知", |
|||
"count": "({0})" |
|||
} |
|||
}, |
|||
"content": { |
|||
"favoriteMenu": { |
|||
"title": "常用", |
|||
"home": "首页", |
|||
"dashboard": "仪表盘", |
|||
"profile": "个人中心", |
|||
"settings": "个人设置", |
|||
"notifiers": "通知消息", |
|||
"manage": "管理菜单", |
|||
"create": "添加菜单", |
|||
"delete": "删除菜单", |
|||
"select": "选择菜单", |
|||
"color": "选择颜色", |
|||
"alias": "自定义别名", |
|||
"icon": "自定义图标" |
|||
}, |
|||
"trends": { |
|||
"title": "最新消息" |
|||
}, |
|||
"todo": { |
|||
"title": "待办事项" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
import type { AuditedEntityDto, IHasConcurrencyStamp } from '@abp/core'; |
|||
|
|||
interface UserFavoriteMenuDto extends AuditedEntityDto<string> { |
|||
aliasName?: string; |
|||
color?: string; |
|||
displayName: string; |
|||
framework: string; |
|||
icon?: string; |
|||
menuId: string; |
|||
name: string; |
|||
path?: string; |
|||
userId: string; |
|||
} |
|||
|
|||
interface UserFavoriteMenuCreateOrUpdateDto { |
|||
aliasName?: string; |
|||
color?: string; |
|||
icon?: string; |
|||
menuId: string; |
|||
} |
|||
|
|||
interface UserFavoriteMenuCreateDto extends UserFavoriteMenuCreateOrUpdateDto { |
|||
framework: string; |
|||
} |
|||
|
|||
interface UserFavoriteMenuUpdateDto |
|||
extends IHasConcurrencyStamp, |
|||
UserFavoriteMenuCreateOrUpdateDto {} |
|||
|
|||
export type { |
|||
UserFavoriteMenuCreateDto, |
|||
UserFavoriteMenuDto, |
|||
UserFavoriteMenuUpdateDto, |
|||
}; |
|||
@ -1,4 +1,5 @@ |
|||
export * from './dataDictionaries'; |
|||
export * from './favorites'; |
|||
export * from './layouts'; |
|||
export * from './menus'; |
|||
export * from './messages'; |
|||
|
|||
@ -1,3 +1,4 @@ |
|||
export { default as SettingDefinitionTable } from './definitions/SettingDefinitionTable.vue'; |
|||
export { default as SettingForm } from './settings/SettingForm.vue'; |
|||
export { default as SystemSetting } from './settings/SystemSetting.vue'; |
|||
export { default as UserSetting } from './settings/UserSetting.vue'; |
|||
|
|||
@ -0,0 +1,40 @@ |
|||
{ |
|||
"name": "@abp/wechat", |
|||
"version": "9.2.0", |
|||
"homepage": "https://github.com/colinin/abp-next-admin", |
|||
"bugs": "https://github.com/colinin/abp-next-admin/issues", |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "git+https://github.com/colinin/abp-next-admin.git", |
|||
"directory": "packages/@abp/wechat" |
|||
}, |
|||
"license": "MIT", |
|||
"type": "module", |
|||
"sideEffects": [ |
|||
"**/*.css" |
|||
], |
|||
"exports": { |
|||
".": { |
|||
"types": "./src/index.ts", |
|||
"default": "./src/index.ts" |
|||
} |
|||
}, |
|||
"dependencies": { |
|||
"@abp/core": "workspace:*", |
|||
"@abp/features": "workspace:*", |
|||
"@abp/request": "workspace:*", |
|||
"@abp/settings": "workspace:*", |
|||
"@abp/ui": "workspace:*", |
|||
"@ant-design/icons-vue": "catalog:", |
|||
"@vben/access": "workspace:*", |
|||
"@vben/common-ui": "workspace:*", |
|||
"@vben/hooks": "workspace:*", |
|||
"@vben/icons": "workspace:*", |
|||
"@vben/layouts": "workspace:*", |
|||
"@vben/locales": "workspace:*", |
|||
"@wecom/jssdk": "catalog:", |
|||
"ant-design-vue": "catalog:", |
|||
"dayjs": "catalog:", |
|||
"vue": "catalog:*" |
|||
} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
export { userWorkWeixinJsSdkApi } from './userWorkWeixinJsSdkApi'; |
|||
export { useWechatSettingsApi } from './useWechatSettingsApi'; |
|||
@ -0,0 +1,40 @@ |
|||
import type { ListResultDto } from '@abp/core'; |
|||
import type { SettingGroup } from '@abp/settings'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useWechatSettingsApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 获取全局设置 |
|||
* @returns 设置数据传输对象列表 |
|||
*/ |
|||
function getGlobalSettingsApi(): Promise<ListResultDto<SettingGroup>> { |
|||
return request<ListResultDto<SettingGroup>>( |
|||
`/api/wechat/setting-management/by-global`, |
|||
{ |
|||
method: 'GET', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 获取租户设置 |
|||
* @returns 设置数据传输对象列表 |
|||
*/ |
|||
function getTenantSettingsApi(): Promise<ListResultDto<SettingGroup>> { |
|||
return request<ListResultDto<SettingGroup>>( |
|||
`/api/wechat/setting-management/by-current-tenant`, |
|||
{ |
|||
method: 'GET', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
getGlobalSettingsApi, |
|||
getTenantSettingsApi, |
|||
}; |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
import type { AgentConfigDto } from '../types/js-sdk'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function userWorkWeixinJsSdkApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 获取企业微信应用配置 |
|||
* @returns 企业微信应用配置Dto |
|||
*/ |
|||
function getAgentConfigApi(): Promise<AgentConfigDto> { |
|||
return request<AgentConfigDto>(`/api/wechat/work/jssdk/agent-config`, { |
|||
method: 'GET', |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
getAgentConfigApi, |
|||
}; |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
<script setup lang="ts"> |
|||
import { useTemplateRef } from 'vue'; |
|||
|
|||
import { useVbenModal } from '@vben/common-ui'; |
|||
|
|||
import { buildUUID } from '@abp/core'; |
|||
import { WWLoginRedirectType, WWLoginType } from '@wecom/jssdk'; |
|||
import * as ww from '@wecom/jssdk'; |
|||
|
|||
import { userWorkWeixinJsSdkApi } from '../../api/userWorkWeixinJsSdkApi'; |
|||
|
|||
const emits = defineEmits<{ |
|||
/** |
|||
* 用户扫码登录成功回调事件 |
|||
* @params code 企业微信授权码 |
|||
*/ |
|||
(event: 'onLogin', code: string): void; |
|||
}>(); |
|||
const wxLoginRef = useTemplateRef<Element>('wxLogin'); |
|||
|
|||
const { getAgentConfigApi } = userWorkWeixinJsSdkApi(); |
|||
|
|||
const [Modal, modalApi] = useVbenModal({ |
|||
onOpenChange(isOpen) { |
|||
if (isOpen) { |
|||
setTimeout(onInitLogin, 200); |
|||
} |
|||
}, |
|||
}); |
|||
|
|||
async function onInitLogin() { |
|||
try { |
|||
modalApi.setState({ loading: true }); |
|||
const agentConfig = await getAgentConfigApi(); |
|||
ww.createWWLoginPanel({ |
|||
el: wxLoginRef.value!, |
|||
params: { |
|||
login_type: WWLoginType.corpApp, |
|||
appid: agentConfig.corpId, |
|||
agentid: agentConfig.agentId, |
|||
// TODO: 是否应改为可配置式? 企业微信仅允许配置一个回调地址, 生产环境应配合反向代理服务器. |
|||
redirect_uri: window.location.href, |
|||
state: buildUUID(), |
|||
redirect_type: WWLoginRedirectType.callback, |
|||
}, |
|||
onLoginSuccess(res) { |
|||
emits('onLogin', res.code); |
|||
}, |
|||
}); |
|||
} finally { |
|||
modalApi.setState({ loading: false }); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Modal :title="$t('AbpAccountOAuth.OAuth:WorkWeixin')"> |
|||
<div ref="wxLogin"></div> |
|||
</Modal> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,2 @@ |
|||
export { default as WechatWorkUserBinder } from './bind-user/index.vue'; |
|||
export { default as WechatSettings } from './settings/index.vue'; |
|||
@ -0,0 +1,37 @@ |
|||
<script setup lang="ts"> |
|||
import type { SettingsUpdateInput } from '@abp/settings'; |
|||
|
|||
import { useAbpStore } from '@abp/core'; |
|||
import { SettingForm, useSettingsApi } from '@abp/settings'; |
|||
|
|||
import { useWechatSettingsApi } from '../../api/useWechatSettingsApi'; |
|||
|
|||
defineOptions({ |
|||
name: 'MaterialInspectSettings', |
|||
}); |
|||
|
|||
const abpStore = useAbpStore(); |
|||
const { getGlobalSettingsApi, getTenantSettingsApi } = useWechatSettingsApi(); |
|||
const { setGlobalSettingsApi, setTenantSettingsApi } = useSettingsApi(); |
|||
|
|||
async function onGet() { |
|||
const getSettingsApi = abpStore.application?.currentTenant.isAvailable |
|||
? getTenantSettingsApi |
|||
: getGlobalSettingsApi; |
|||
const { items } = await getSettingsApi(); |
|||
return items; |
|||
} |
|||
|
|||
async function onSubmit(input: SettingsUpdateInput) { |
|||
const setSettingsApi = abpStore.application?.currentTenant.isAvailable |
|||
? setTenantSettingsApi |
|||
: setGlobalSettingsApi; |
|||
await setSettingsApi(input); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<SettingForm :get-api="onGet" :submit-api="onSubmit" /> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,2 @@ |
|||
export * from './api'; |
|||
export * from './components'; |
|||
@ -0,0 +1,6 @@ |
|||
interface AgentConfigDto { |
|||
agentId: string; |
|||
corpId: string; |
|||
} |
|||
|
|||
export type { AgentConfigDto }; |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/tsconfig/web.json", |
|||
"include": ["src"], |
|||
"exclude": ["node_modules"] |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,20 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net9.0</TargetFramework> |
|||
<AssemblyName>LINGYUN.Abp.AspNetCore.Auditing</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.AspNetCore.Auditing</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.AspNetCore" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,19 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Auditing; |
|||
public class AbpAspNetCoreAuditingHeaderOptions |
|||
{ |
|||
/// <summary>
|
|||
/// 是否在审计日志中记录Http请求头,默认: true
|
|||
/// </summary>
|
|||
public bool IsEnabled { get; set; } |
|||
/// <summary>
|
|||
/// 要记录的Http请求头
|
|||
/// </summary>
|
|||
public IList<string> HttpHeaders { get; } |
|||
public AbpAspNetCoreAuditingHeaderOptions() |
|||
{ |
|||
IsEnabled = true; |
|||
HttpHeaders = new List<string>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using Volo.Abp.AspNetCore; |
|||
using Volo.Abp.Auditing; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Auditing; |
|||
|
|||
[DependsOn(typeof(AbpAspNetCoreModule))] |
|||
public class AbpAspNetCoreAuditingModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpAuditingOptions>(options => |
|||
{ |
|||
options.Contributors.Add(new AspNetCoreRecordHeaderAuditLogContributor()); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Immutable; |
|||
using Volo.Abp.Auditing; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Auditing; |
|||
public class AspNetCoreRecordHeaderAuditLogContributor : AuditLogContributor, ITransientDependency |
|||
{ |
|||
private const string HttpHeaderRecordKey = "HttpHeaders"; |
|||
|
|||
public AspNetCoreRecordHeaderAuditLogContributor() |
|||
{ |
|||
} |
|||
|
|||
public override void PreContribute(AuditLogContributionContext context) |
|||
{ |
|||
var options = context.ServiceProvider.GetRequiredService<IOptions<AbpAspNetCoreAuditingHeaderOptions>>(); |
|||
if (!options.Value.IsEnabled) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var httpContext = context.ServiceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext; |
|||
if (httpContext == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (context.AuditInfo.HasProperty(HttpHeaderRecordKey)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var headerRcords = new Dictionary<string, string>(); |
|||
var httpHeaders = httpContext.Request.Headers.ToImmutableDictionary(); |
|||
|
|||
foreach (var headerKey in options.Value.HttpHeaders) |
|||
{ |
|||
if (httpHeaders.TryGetValue(headerKey, out var headers)) |
|||
{ |
|||
headerRcords[headerKey] = headers.JoinAsString(";"); |
|||
} |
|||
} |
|||
|
|||
context.AuditInfo.SetProperty(HttpHeaderRecordKey, headerRcords); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
# LINGYUN.Abp.AspNetCore.Auditing |
|||
|
|||
审计日期扩展模块, 用于在审计日志中加入特定的Http请求头记录 |
|||
|
|||
## 模块引用 |
|||
|
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpAspNetCoreAuditingModule))] |
|||
public class YouProjectModule : AbpModule |
|||
{ |
|||
// other |
|||
} |
|||
``` |
|||
|
|||
## 配置项 |
|||
|
|||
* AbpAspNetCoreAuditingHeaderOptions.IsEnabled 是否在审计日志中记录Http请求头,默认: true |
|||
* AbpAspNetCoreAuditingHeaderOptions.HttpHeaders 需要在审计日志中记录的Http请求头列表 |
|||
File diff suppressed because it is too large
@ -0,0 +1,63 @@ |
|||
using System; |
|||
using Microsoft.EntityFrameworkCore.Migrations; |
|||
|
|||
#nullable disable |
|||
|
|||
namespace LY.MicroService.Applications.Single.EntityFrameworkCore.MySql.Migrations |
|||
{ |
|||
/// <inheritdoc />
|
|||
public partial class UpgradeAbpFrameworkTo931 : Migration |
|||
{ |
|||
/// <inheritdoc />
|
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.AlterColumn<Guid>( |
|||
name: "EntityId", |
|||
table: "Demo_BooksAuths", |
|||
type: "char(64)", |
|||
maxLength: 64, |
|||
nullable: false, |
|||
collation: "ascii_general_ci", |
|||
oldClrType: typeof(string), |
|||
oldType: "char(64)", |
|||
oldMaxLength: 64) |
|||
.OldAnnotation("MySql:CharSet", "utf8mb4"); |
|||
|
|||
migrationBuilder.CreateTable( |
|||
name: "AbpAuditLogExcelFiles", |
|||
columns: table => new |
|||
{ |
|||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"), |
|||
TenantId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"), |
|||
FileName = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: true) |
|||
.Annotation("MySql:CharSet", "utf8mb4"), |
|||
CreationTime = table.Column<DateTime>(type: "datetime(6)", nullable: false), |
|||
CreatorId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci") |
|||
}, |
|||
constraints: table => |
|||
{ |
|||
table.PrimaryKey("PK_AbpAuditLogExcelFiles", x => x.Id); |
|||
}) |
|||
.Annotation("MySql:CharSet", "utf8mb4"); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected override void Down(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropTable( |
|||
name: "AbpAuditLogExcelFiles"); |
|||
|
|||
migrationBuilder.AlterColumn<string>( |
|||
name: "EntityId", |
|||
table: "Demo_BooksAuths", |
|||
type: "char(64)", |
|||
maxLength: 64, |
|||
nullable: false, |
|||
oldClrType: typeof(Guid), |
|||
oldType: "char(64)", |
|||
oldMaxLength: 64) |
|||
.Annotation("MySql:CharSet", "utf8mb4") |
|||
.OldAnnotation("Relational:Collation", "ascii_general_ci"); |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue