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> |
<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' }); |
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> |
</script> |
||||
|
|
||||
<template> |
<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> |
</template> |
||||
|
|||||
@ -1,15 +1,98 @@ |
|||||
<script lang="ts" setup> |
<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({ |
defineOptions({ |
||||
name: 'Vben5AccountMySettings', |
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> |
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<Page> |
<Page> |
||||
<MySetting /> |
<MySetting :bind-items="externalLogins" @on-bind-init="onInit" /> |
||||
|
<WechatWorkUserBindModal @on-login="onBindWorkWeixin" /> |
||||
</Page> |
</Page> |
||||
</template> |
</template> |
||||
|
|||||
@ -1,266 +1,32 @@ |
|||||
<script lang="ts" setup> |
<script setup lang="ts"> |
||||
import type { |
import type { FavoriteMenu } from '@abp/platform'; |
||||
WorkbenchProjectItem, |
|
||||
WorkbenchQuickNavItem, |
|
||||
WorkbenchTodoItem, |
|
||||
WorkbenchTrendItem, |
|
||||
} from '@vben/common-ui'; |
|
||||
|
|
||||
import { ref } from 'vue'; |
|
||||
import { useRouter } from 'vue-router'; |
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 { openWindow } from '@vben/utils'; |
||||
|
|
||||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue'; |
import { Workbench } from '@abp/platform'; |
||||
|
|
||||
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', |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
const router = useRouter(); |
const router = useRouter(); |
||||
|
function navTo(menu: FavoriteMenu) { |
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整 |
if (menu.path?.startsWith('http')) { |
||||
// This is a sample method, adjust according to the actual project requirements |
openWindow(menu.path); |
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) { |
|
||||
if (nav.url?.startsWith('http')) { |
|
||||
openWindow(nav.url); |
|
||||
return; |
return; |
||||
} |
} |
||||
if (nav.url?.startsWith('/')) { |
if (menu.path?.startsWith('/')) { |
||||
router.push(nav.url).catch((error) => { |
router.push(menu.path).catch((error) => { |
||||
console.error('Navigation failed:', error); |
console.error('Navigation failed:', error); |
||||
}); |
}); |
||||
} else { |
} else { |
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`); |
console.warn( |
||||
|
`Unknown URL for navigation item: ${menu.displayName} -> ${menu.path}`, |
||||
|
); |
||||
} |
} |
||||
} |
} |
||||
</script> |
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<div class="p-5"> |
<Workbench @nav-to="navTo" /> |
||||
<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> |
|
||||
</template> |
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
|
|||||
@ -1,7 +1,5 @@ |
|||||
export { useAccountApi } from './useAccountApi'; |
export { useAccountApi } from './useAccountApi'; |
||||
|
export { useExternalLoginsApi } from './useExternalLoginsApi'; |
||||
export { useMySessionApi } from './useMySessionApi'; |
export { useMySessionApi } from './useMySessionApi'; |
||||
export { usePhoneLoginApi } from './usePhoneLoginApi'; |
|
||||
export { useProfileApi } from './useProfileApi'; |
export { useProfileApi } from './useProfileApi'; |
||||
export { useQrCodeLoginApi } from './useQrCodeLoginApi'; |
export { useScanQrCodeApi } from './useScanQrCodeApi'; |
||||
export { useTokenApi } from './useTokenApi'; |
|
||||
export { useUserInfoApi } from './useUserInfoApi'; |
|
||||
|
|||||
@ -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 './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 './account'; |
||||
|
export * from './bind'; |
||||
|
export * from './external-logins'; |
||||
export * from './profile'; |
export * from './profile'; |
||||
export * from './token'; |
export * from './token'; |
||||
export * from './user'; |
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 './api'; |
||||
export * from './components'; |
export * from './components'; |
||||
export * from './hooks'; |
export * from './hooks'; |
||||
|
export * from './locales'; |
||||
export * from './types'; |
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 './dataDictionaries'; |
||||
|
export * from './favorites'; |
||||
export * from './layouts'; |
export * from './layouts'; |
||||
export * from './menus'; |
export * from './menus'; |
||||
export * from './messages'; |
export * from './messages'; |
||||
|
|||||
@ -1,3 +1,4 @@ |
|||||
export { default as SettingDefinitionTable } from './definitions/SettingDefinitionTable.vue'; |
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 SystemSetting } from './settings/SystemSetting.vue'; |
||||
export { default as UserSetting } from './settings/UserSetting.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