committed by
GitHub
338 changed files with 12559 additions and 3601 deletions
@ -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,31 @@ |
|||||
|
import { isDate, isNumber } from './is'; |
||||
|
|
||||
|
export function sorter( |
||||
|
a: Record<string, any>, |
||||
|
b: Record<string, any>, |
||||
|
field: string, |
||||
|
): number { |
||||
|
if (!a[field] && !b[field]) { |
||||
|
return 0; |
||||
|
} |
||||
|
if (a[field] && !b[field]) { |
||||
|
return 1; |
||||
|
} |
||||
|
if (b[field] && !a[field]) { |
||||
|
return -1; |
||||
|
} |
||||
|
const va = a[field]; |
||||
|
const vb = b[field]; |
||||
|
if (isDate(va) && isDate(vb)) { |
||||
|
return va.getTime() - vb.getTime(); |
||||
|
} |
||||
|
if (isNumber(va) && isNumber(vb)) { |
||||
|
return va - vb; |
||||
|
} |
||||
|
if (Array.isArray(va) && Array.isArray(vb)) { |
||||
|
return va.length - vb.length; |
||||
|
} |
||||
|
return String(va).localeCompare(String(vb)); |
||||
|
} |
||||
|
|
||||
|
export { default as sortby } from 'lodash.sortby'; |
||||
@ -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, |
||||
|
}; |
||||
|
} |
||||
@ -1,3 +1,16 @@ |
|||||
|
import type { MenuDto } from '../../types/menus'; |
||||
|
|
||||
type MenuSubject = 'role' | 'user'; |
type MenuSubject = 'role' | 'user'; |
||||
|
|
||||
export type { MenuSubject }; |
type EditMenu = { |
||||
|
id?: string; |
||||
|
layoutId?: string; |
||||
|
parentId?: string; |
||||
|
}; |
||||
|
|
||||
|
type MenuDrawerState = { |
||||
|
editMenu?: EditMenu; |
||||
|
rootMenus: MenuDto[]; |
||||
|
}; |
||||
|
|
||||
|
export type { MenuDrawerState, MenuSubject }; |
||||
|
|||||
@ -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'; |
||||
|
|||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue