diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac4fe231d..60742282a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: - "**.csproj" env: - DOTNET_VERSION: "9.0.301" + DOTNET_VERSION: "9.0.304" jobs: build: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a5cf75644..9020906b2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,7 +4,7 @@ on: pull_request: branches: [ main ] env: - DOTNET_VERSION: "9.0.301" + DOTNET_VERSION: "9.0.304" jobs: publish: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 449bee959..b68f94f9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,4 +14,4 @@ jobs: with: repo_token: "${{ secrets.GITHUB_TOKEN }}" prerelease: false - automatic_release_tag: "9.2.3" + automatic_release_tag: "9.3.5" diff --git a/Directory.Packages.props b/Directory.Packages.props index 67b29f6e7..aacc3e141 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,16 +3,16 @@ 8.3.5 2.15.2 3.3.5 - 9.2.3 - 9.2.3 - 9.0.4 - 9.0.4 - 9.0.4 + 9.3.5 + 9.3.5 + 9.0.5 + 9.0.5 + 9.0.5 true - + @@ -63,7 +63,8 @@ - + + @@ -157,7 +158,7 @@ - + @@ -174,8 +175,9 @@ - - + + + @@ -264,7 +266,7 @@ - + @@ -276,7 +278,7 @@ - + @@ -299,8 +301,8 @@ - - + + diff --git a/apps/vben5/.vscode/extensions.json b/apps/vben5/.vscode/extensions.json new file mode 100644 index 000000000..e8dc9ed9b --- /dev/null +++ b/apps/vben5/.vscode/extensions.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" + ] +} diff --git a/apps/vben5/.vscode/global.code-snippets b/apps/vben5/.vscode/global.code-snippets new file mode 100644 index 000000000..7604b0148 --- /dev/null +++ b/apps/vben5/.vscode/global.code-snippets @@ -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": "", + "const props = defineProps<{", + " modelValue?: boolean,", + "}>()", + "$1", + "", + "", + "", + " ", + " ", + " ", + "", + ], + }, + "vue-computed": { + "scope": "javascript,typescript,vue", + "prefix": "com", + "body": ["computed(() => { $1 })"], + }, +} diff --git a/apps/vben5/.vscode/launch.json b/apps/vben5/.vscode/launch.json new file mode 100644 index 000000000..d868ef52b --- /dev/null +++ b/apps/vben5/.vscode/launch.json @@ -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" + } + ] +} diff --git a/apps/vben5/.vscode/settings.json b/apps/vben5/.vscode/settings.json new file mode 100644 index 000000000..f38c42781 --- /dev/null +++ b/apps/vben5/.vscode/settings.json @@ -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" + ] +} diff --git a/apps/vben5/apps/app-antd/package.json b/apps/vben5/apps/app-antd/package.json index 03fa292ee..efbe91733 100644 --- a/apps/vben5/apps/app-antd/package.json +++ b/apps/vben5/apps/app-antd/package.json @@ -46,6 +46,7 @@ "@abp/text-templating": "workspace:*", "@abp/ui": "workspace:*", "@abp/webhooks": "workspace:*", + "@abp/wechat": "workspace:*", "@vben-core/shadcn-ui": "workspace:*", "@vben/access": "workspace:*", "@vben/common-ui": "workspace:*", @@ -66,6 +67,7 @@ "dayjs": "catalog:", "pinia": "catalog:", "vue": "catalog:", - "vue-router": "catalog:" + "vue-router": "catalog:", + "vue3-colorpicker": "catalog:" } } diff --git a/apps/vben5/apps/app-antd/src/adapter/component/index.ts b/apps/vben5/apps/app-antd/src/adapter/component/index.ts index 4ab140011..e98ba7f61 100644 --- a/apps/vben5/apps/app-antd/src/adapter/component/index.ts +++ b/apps/vben5/apps/app-antd/src/adapter/component/index.ts @@ -31,6 +31,12 @@ const Button = defineAsyncComponent(() => import('ant-design-vue/es/button')); const Checkbox = defineAsyncComponent( () => import('ant-design-vue/es/checkbox'), ); +const ColorPicker = defineAsyncComponent(() => + import('vue3-colorpicker').then((res) => { + import('vue3-colorpicker/style.css'); + return res.ColorPicker; + }), +); const CheckboxGroup = defineAsyncComponent(() => import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup), ); @@ -117,6 +123,7 @@ export type ComponentType = | 'AutoComplete' | 'Checkbox' | 'CheckboxGroup' + | 'ColorPicker' | 'DatePicker' | 'DefaultButton' | 'Divider' @@ -182,6 +189,7 @@ async function initComponentAdapter() { AutoComplete, Checkbox, CheckboxGroup, + ColorPicker, DatePicker, // 自定义默认按钮 DefaultButton: (props, { attrs, slots }) => { diff --git a/apps/vben5/apps/app-antd/src/adapter/request/index.ts b/apps/vben5/apps/app-antd/src/adapter/request/index.ts index e0c13d6a5..1fe3242a1 100644 --- a/apps/vben5/apps/app-antd/src/adapter/request/index.ts +++ b/apps/vben5/apps/app-antd/src/adapter/request/index.ts @@ -37,7 +37,8 @@ export function initRequestClient() { async function doRefreshToken() { const authStore = useAuthStore(); try { - return await authStore.refreshSession(); + const token = await authStore.refreshSession(); + return token ?? ''; } catch { console.warn('The refresh token has expired or is unavailable.'); } @@ -60,6 +61,9 @@ export function initRequestClient() { if (abpStore.tenantId) { config.headers.__tenant = abpStore.tenantId; } + if (abpStore.xsrfToken) { + config.headers.RequestVerificationToken = abpStore.xsrfToken; + } return config; }, }); diff --git a/apps/vben5/apps/app-antd/src/layouts/basic.vue b/apps/vben5/apps/app-antd/src/layouts/basic.vue index 99d4b38f8..a9059508d 100644 --- a/apps/vben5/apps/app-antd/src/layouts/basic.vue +++ b/apps/vben5/apps/app-antd/src/layouts/basic.vue @@ -5,14 +5,8 @@ import { computed, ref, watch } from 'vue'; import { useRouter } from 'vue-router'; import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; -import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants'; import { useWatermark } from '@vben/hooks'; -import { - BookOpenText, - CircleHelp, - createIconifyIcon, - MdiGithub, -} from '@vben/icons'; +import { createIconifyIcon } from '@vben/icons'; import { BasicLayout, LockScreen, @@ -21,7 +15,6 @@ import { } from '@vben/layouts'; import { preferences } from '@vben/preferences'; import { useAccessStore, useUserStore } from '@vben/stores'; -import { openWindow } from '@vben/utils'; import { useAbpStore } from '@abp/core'; @@ -32,36 +25,7 @@ import LoginForm from '#/views/_core/authentication/login.vue'; const UserSettingsIcon = createIconifyIcon('tdesign:user-setting'); -const notifications = ref([ - { - avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB', - date: '3小时前', - isRead: true, - message: '描述信息描述信息描述信息', - title: '收到了 14 份新周报', - }, - { - avatar: 'https://avatar.vercel.sh/1', - date: '刚刚', - isRead: false, - message: '描述信息描述信息描述信息', - title: '朱偏右 回复了你', - }, - { - avatar: 'https://avatar.vercel.sh/1', - date: '2024-01-01', - isRead: false, - message: '描述信息描述信息描述信息', - title: '曲丽丽 评论了你', - }, - { - avatar: 'https://avatar.vercel.sh/satori', - date: '1天前', - isRead: false, - message: '描述信息描述信息描述信息', - title: '代办提醒', - }, -]); +const notifications = ref([]); useSessions(); @@ -83,33 +47,6 @@ const menus = computed(() => [ icon: UserSettingsIcon, text: $t('abp.account.settings.title'), }, - { - handler: () => { - openWindow(VBEN_DOC_URL, { - target: '_blank', - }); - }, - icon: BookOpenText, - text: $t('ui.widgets.document'), - }, - { - handler: () => { - openWindow(VBEN_GITHUB_URL, { - target: '_blank', - }); - }, - icon: MdiGithub, - text: 'GitHub', - }, - { - handler: () => { - openWindow(`${VBEN_GITHUB_URL}/issues`, { - target: '_blank', - }); - }, - icon: CircleHelp, - text: $t('ui.widgets.qa'), - }, ]); const userInfo = computed(() => { diff --git a/apps/vben5/apps/app-antd/src/locales/index.ts b/apps/vben5/apps/app-antd/src/locales/index.ts index f221600e7..7fe299ca0 100644 --- a/apps/vben5/apps/app-antd/src/locales/index.ts +++ b/apps/vben5/apps/app-antd/src/locales/index.ts @@ -15,6 +15,7 @@ import { preferences } from '@vben/preferences'; import { useAbpStore } from '@abp/core'; import { useLocalizationsApi } from '@abp/localization'; +import { loadPaltformMessages } from '@abp/platform'; import antdEnLocale from 'ant-design-vue/es/locale/en_US'; import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN'; import dayjs from 'dayjs'; @@ -34,13 +35,17 @@ const localesMap = loadLocalesMapFromDir( * @param lang */ async function loadMessages(lang: SupportedLanguagesType) { - const [appLocaleMessages, _, abpLocales] = await Promise.all([ - localesMap[lang]?.(), - loadThirdPartyMessage(lang), - loadAbpLocale(lang), - ]); + const [appLocaleMessages, platformLocales, _, abpLocales] = await Promise.all( + [ + localesMap[lang]?.(), + loadPaltformMessages(lang), + loadThirdPartyMessage(lang), + loadAbpLocale(lang), + ], + ); return { ...appLocaleMessages?.default, + ...platformLocales?.default, ...abpLocales, }; } diff --git a/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json b/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json index 277f4585d..49dced4c6 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json @@ -142,5 +142,9 @@ "title": "Object storage", "containers": "Containers", "objects": "Files" + }, + "wechat": { + "title": "WeChat", + "settings": "Settings" } } diff --git a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json index 0665783bc..16bb7ec01 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json @@ -142,5 +142,9 @@ "title": "对象存储", "containers": "容器管理", "objects": "文件管理" + }, + "wechat": { + "title": "微信集成", + "settings": "微信设置" } } diff --git a/apps/vben5/apps/app-antd/src/preferences.ts b/apps/vben5/apps/app-antd/src/preferences.ts index 4ee0c5515..94d2a3ca8 100644 --- a/apps/vben5/apps/app-antd/src/preferences.ts +++ b/apps/vben5/apps/app-antd/src/preferences.ts @@ -9,7 +9,15 @@ export const overridesPreferences = defineOverridesPreferences({ // overrides app: { accessMode: 'backend', + defaultHomePath: '/workspace', enableRefreshToken: true, name: import.meta.env.VITE_APP_TITLE, }, + theme: { + mode: 'auto', + radius: '0.25', + }, + widget: { + notification: false, + }, }); diff --git a/apps/vben5/apps/app-antd/src/store/auth.ts b/apps/vben5/apps/app-antd/src/store/auth.ts index 7492529a5..3b3452809 100644 --- a/apps/vben5/apps/app-antd/src/store/auth.ts +++ b/apps/vben5/apps/app-antd/src/store/auth.ts @@ -9,14 +9,7 @@ import { LOGIN_PATH } from '@vben/constants'; import { preferences } from '@vben/preferences'; import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; -import { - useOidcClient, - usePhoneLoginApi, - useProfileApi, - useQrCodeLoginApi, - useTokenApi, - useUserInfoApi, -} from '@abp/account'; +import { useOAuthService, useProfileApi } from '@abp/account'; import { Events, useAbpStore, useEventBus } from '@abp/core'; import { notification } from 'ant-design-vue'; import { defineStore } from 'pinia'; @@ -26,48 +19,35 @@ import { $t } from '#/locales'; export const useAuthStore = defineStore('auth', () => { const { publish } = useEventBus(); - const { loginApi, refreshTokenApi } = useTokenApi(); - const { loginApi: qrcodeLoginApi } = useQrCodeLoginApi(); - const { loginApi: phoneLoginApi } = usePhoneLoginApi(); - const { getUserInfoApi } = useUserInfoApi(); const { getConfigApi } = useAbpConfigApi(); const { getPictureApi } = useProfileApi(); const accessStore = useAccessStore(); const userStore = useUserStore(); const abpStore = useAbpStore(); const router = useRouter(); - const oidcClient = useOidcClient(); + const oAuthService = useOAuthService(); const loginLoading = ref(false); async function refreshSession() { - if (await oidcClient.getAccessToken()) { - const user = await oidcClient.refreshToken(); + if (await oAuthService.getAccessToken()) { + const user = await oAuthService.refreshToken(); const newToken = `${user?.token_type} ${user?.access_token}`; accessStore.setAccessToken(newToken); if (user?.refresh_token) { accessStore.setRefreshToken(user.refresh_token); } return newToken; - } else { - const { accessToken, tokenType, refreshToken } = await refreshTokenApi({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - refreshToken: accessStore.refreshToken!, - }); - const newToken = `${tokenType} ${accessToken}`; - accessStore.setAccessToken(newToken); - accessStore.setRefreshToken(refreshToken); - return newToken; } } async function oidcLogin() { - await oidcClient.login(); + await oAuthService.login(); } async function oidcCallback() { try { - const user = await oidcClient.handleCallback(); + const user = await oAuthService.handleCallback(); return await _loginSuccess({ accessToken: user.access_token, tokenType: user.token_type, @@ -87,8 +67,17 @@ export const useAuthStore = defineStore('auth', () => { ) { try { loginLoading.value = true; - const result = await qrcodeLoginApi({ key, tenantId }); - return await _loginSuccess(result, onSuccess); + const user = await oAuthService.loginByQrCode({ key, tenantId }); + return await _loginSuccess( + { + accessToken: user.access_token, + tokenType: user.token_type, + refreshToken: user.refresh_token ?? '', + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expiresIn: user.expires_in!, + }, + onSuccess, + ); } finally { loginLoading.value = false; } @@ -101,8 +90,17 @@ export const useAuthStore = defineStore('auth', () => { ) { try { loginLoading.value = true; - const result = await phoneLoginApi({ phoneNumber, code }); - return await _loginSuccess(result, onSuccess); + const user = await oAuthService.loginBySmsCode({ phoneNumber, code }); + return await _loginSuccess( + { + accessToken: user.access_token, + tokenType: user.token_type, + refreshToken: user.refresh_token ?? '', + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expiresIn: user.expires_in!, + }, + onSuccess, + ); } finally { loginLoading.value = false; } @@ -119,8 +117,17 @@ export const useAuthStore = defineStore('auth', () => { ) { try { loginLoading.value = true; - const result = await loginApi(params as any); - return await _loginSuccess(result, onSuccess); + const user = await oAuthService.loginByPassword(params as any); + return await _loginSuccess( + { + accessToken: user.access_token, + tokenType: user.token_type, + refreshToken: user.refresh_token ?? '', + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expiresIn: user.expires_in!, + }, + onSuccess, + ); } finally { loginLoading.value = false; } @@ -128,9 +135,11 @@ export const useAuthStore = defineStore('auth', () => { async function logout(redirect: boolean = true) { try { - if (await oidcClient.getAccessToken()) { + if (await oAuthService.getAccessToken()) { accessStore.setAccessToken(null); - await oidcClient.logout(); + await oAuthService.logout(); + } else { + await oAuthService.revokeTokens(); } } catch { // 不做任何处理 @@ -153,7 +162,11 @@ export const useAuthStore = defineStore('auth', () => { async function fetchUserInfo() { let userInfo: null | (UserInfo & { [key: string]: any }) = null; - const userInfoRes = await getUserInfoApi(); + let userInfoRes: { [key: string]: any } = {}; + const user = await oAuthService.getUser(); + if (user) { + userInfoRes = user.profile; + } const abpConfig = await getConfigApi(); const picture = await getPictureApi(); userInfo = { diff --git a/apps/vben5/apps/app-antd/src/views/_core/fallback/not-found.vue b/apps/vben5/apps/app-antd/src/views/_core/fallback/not-found.vue index 4d178e9cb..9fa96d7f1 100644 --- a/apps/vben5/apps/app-antd/src/views/_core/fallback/not-found.vue +++ b/apps/vben5/apps/app-antd/src/views/_core/fallback/not-found.vue @@ -1,9 +1,51 @@ - + + + + + + {{ $t('common.backToHome') }} + + + + {{ $t('common.logout') }} + + + + diff --git a/apps/vben5/apps/app-antd/src/views/account/my-settings/index.vue b/apps/vben5/apps/app-antd/src/views/account/my-settings/index.vue index 29ccab460..5dda522cc 100644 --- a/apps/vben5/apps/app-antd/src/views/account/my-settings/index.vue +++ b/apps/vben5/apps/app-antd/src/views/account/my-settings/index.vue @@ -1,15 +1,98 @@ - + + diff --git a/apps/vben5/apps/app-antd/src/views/dashboard/workspace/index.vue b/apps/vben5/apps/app-antd/src/views/dashboard/workspace/index.vue index b95d61381..9055b39d8 100644 --- a/apps/vben5/apps/app-antd/src/views/dashboard/workspace/index.vue +++ b/apps/vben5/apps/app-antd/src/views/dashboard/workspace/index.vue @@ -1,266 +1,32 @@ - - - - - 早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧! - - 今日晴,20℃ - 32℃! - - - - - - - - - - - - - - - - + + + diff --git a/apps/vben5/packages/@abp/account/src/api/index.ts b/apps/vben5/packages/@abp/account/src/api/index.ts index 54e3e8ead..3810e2eb2 100644 --- a/apps/vben5/packages/@abp/account/src/api/index.ts +++ b/apps/vben5/packages/@abp/account/src/api/index.ts @@ -1,7 +1,5 @@ export { useAccountApi } from './useAccountApi'; +export { useExternalLoginsApi } from './useExternalLoginsApi'; export { useMySessionApi } from './useMySessionApi'; -export { usePhoneLoginApi } from './usePhoneLoginApi'; export { useProfileApi } from './useProfileApi'; -export { useQrCodeLoginApi } from './useQrCodeLoginApi'; -export { useTokenApi } from './useTokenApi'; -export { useUserInfoApi } from './useUserInfoApi'; +export { useScanQrCodeApi } from './useScanQrCodeApi'; diff --git a/apps/vben5/packages/@abp/account/src/api/useExternalLoginsApi.ts b/apps/vben5/packages/@abp/account/src/api/useExternalLoginsApi.ts new file mode 100644 index 000000000..ff2b0122f --- /dev/null +++ b/apps/vben5/packages/@abp/account/src/api/useExternalLoginsApi.ts @@ -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 } + */ + async function bindWorkWeixinApi( + input: WorkWeixinLoginBindInput, + ): Promise { + return await request(`/api/account/oauth/work-weixin/bind`, { + method: 'POST', + data: input, + }); + } + + /** + * 获取外部登录提供者列表 + * @returns 外部登录提供者列表 + */ + async function getExternalLoginsApi(): Promise { + return await request( + `/api/account/external-logins`, + { + method: 'GET', + }, + ); + } + + /** + * 移除外部登录提供者 + * @returns { Promise } + */ + async function removeExternalLoginApi( + input: RemoveExternalLoginInput, + ): Promise { + return await request(`/api/account/external-logins/remove`, { + method: 'DELETE', + params: input, + }); + } + + return { + cancel, + bindWorkWeixinApi, + getExternalLoginsApi, + removeExternalLoginApi, + }; +} diff --git a/apps/vben5/packages/@abp/account/src/api/usePhoneLoginApi.ts b/apps/vben5/packages/@abp/account/src/api/usePhoneLoginApi.ts deleted file mode 100644 index 0d6be68b3..000000000 --- a/apps/vben5/packages/@abp/account/src/api/usePhoneLoginApi.ts +++ /dev/null @@ -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('/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, - }; -} diff --git a/apps/vben5/packages/@abp/account/src/api/useQrCodeLoginApi.ts b/apps/vben5/packages/@abp/account/src/api/useQrCodeLoginApi.ts deleted file mode 100644 index 3acf36957..000000000 --- a/apps/vben5/packages/@abp/account/src/api/useQrCodeLoginApi.ts +++ /dev/null @@ -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 { - return request('/api/account/qrcode/generate', { - method: 'POST', - }); - } - - /** - * 检查二维码状态 - * @param key 二维码Key - * @returns 二维码信息 - */ - function checkCodeApi(key: string): Promise { - return request(`/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('/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, - }; -} diff --git a/apps/vben5/packages/@abp/account/src/api/useScanQrCodeApi.ts b/apps/vben5/packages/@abp/account/src/api/useScanQrCodeApi.ts new file mode 100644 index 000000000..c28fff84b --- /dev/null +++ b/apps/vben5/packages/@abp/account/src/api/useScanQrCodeApi.ts @@ -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 { + return request('/api/account/qrcode/generate', { + method: 'POST', + }); + } + + /** + * 检查二维码状态 + * @param key 二维码Key + * @returns 二维码信息 + */ + function checkCodeApi(key: string): Promise { + return request(`/api/account/qrcode/${key}/check`, { + method: 'GET', + }); + } + + return { + cancel, + checkCodeApi, + generateApi, + }; +} diff --git a/apps/vben5/packages/@abp/account/src/api/useTokenApi.ts b/apps/vben5/packages/@abp/account/src/api/useTokenApi.ts deleted file mode 100644 index e41e84c10..000000000 --- a/apps/vben5/packages/@abp/account/src/api/useTokenApi.ts +++ /dev/null @@ -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 { - const { audience, clientId, clientSecret } = useAppConfig( - import.meta.env, - import.meta.env.PROD, - ); - const result = await request('/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('/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, - }; -} diff --git a/apps/vben5/packages/@abp/account/src/api/useUserInfoApi.ts b/apps/vben5/packages/@abp/account/src/api/useUserInfoApi.ts deleted file mode 100644 index aa7c5adb3..000000000 --- a/apps/vben5/packages/@abp/account/src/api/useUserInfoApi.ts +++ /dev/null @@ -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 { - const result = await request('/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, - }; -} diff --git a/apps/vben5/packages/@abp/account/src/components/MySetting.vue b/apps/vben5/packages/@abp/account/src/components/MySetting.vue index ee412c254..848cf6630 100644 --- a/apps/vben5/packages/@abp/account/src/components/MySetting.vue +++ b/apps/vben5/packages/@abp/account/src/components/MySetting.vue @@ -1,8 +1,9 @@ - + + + + + + {{ button.title }} + + + + + diff --git a/apps/vben5/packages/@abp/account/src/components/components/SecuritySettings.vue b/apps/vben5/packages/@abp/account/src/components/components/SecuritySettings.vue index 65ac3940b..ce65c2368 100644 --- a/apps/vben5/packages/@abp/account/src/components/components/SecuritySettings.vue +++ b/apps/vben5/packages/@abp/account/src/components/components/SecuritySettings.vue @@ -86,13 +86,8 @@ onMounted(onGet); - - {{ - $t('abp.account.settings.security.password') - }} - - + :title="$t('abp.account.settings.security.password')" + /> diff --git a/apps/vben5/packages/@abp/account/src/hooks/index.ts b/apps/vben5/packages/@abp/account/src/hooks/index.ts index 25bce3f27..556dbb37b 100644 --- a/apps/vben5/packages/@abp/account/src/hooks/index.ts +++ b/apps/vben5/packages/@abp/account/src/hooks/index.ts @@ -1,2 +1,2 @@ export * from './useOAuthError'; -export * from './useOidcClient'; +export * from './useOAuthService'; diff --git a/apps/vben5/packages/@abp/account/src/hooks/useOAuthError.ts b/apps/vben5/packages/@abp/account/src/hooks/useOAuthError.ts index 972f8274d..a1e821002 100644 --- a/apps/vben5/packages/@abp/account/src/hooks/useOAuthError.ts +++ b/apps/vben5/packages/@abp/account/src/hooks/useOAuthError.ts @@ -23,7 +23,8 @@ export function useOAuthError() { return $t('abp.oauth.errors.requiresTwoFactor'); } // Token已失效 - case 'The token is no longer valid.': { + case 'The token is no longer valid.': + case 'The user is no longer allowed to sign in.': { return $t('abp.oauth.errors.tokenHasExpired'); } // 用户尝试登录次数太多,用户被锁定 diff --git a/apps/vben5/packages/@abp/account/src/hooks/useOidcClient.ts b/apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts similarity index 53% rename from apps/vben5/packages/@abp/account/src/hooks/useOidcClient.ts rename to apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts index f4090b44e..92e064aeb 100644 --- a/apps/vben5/packages/@abp/account/src/hooks/useOidcClient.ts +++ b/apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts @@ -1,14 +1,36 @@ +import type { + PasswordTokenRequestModel, + PhoneNumberTokenRequest, + QrCodeTokenRequest, +} from '../types/token'; + import { userManager } from '../utils/auth'; -export function useOidcClient() { +export function useOAuthService() { async function login() { return userManager.signinRedirect(); } + async function loginByPassword(input: PasswordTokenRequestModel) { + return userManager.signinResourceOwnerCredentials(input); + } + + async function loginBySmsCode(input: PhoneNumberTokenRequest) { + return userManager.signinSmsCode(input); + } + + async function loginByQrCode(input: QrCodeTokenRequest) { + return userManager.signinQrCode(input); + } + async function logout() { return userManager.signoutRedirect(); } + async function revokeTokens() { + return userManager.revokeTokens(['access_token', 'refresh_token']); + } + async function refreshToken() { return userManager.signinSilent(); } @@ -33,8 +55,12 @@ export function useOidcClient() { return { login, + loginByPassword, + loginBySmsCode, + loginByQrCode, logout, refreshToken, + revokeTokens, getAccessToken, isAuthenticated, handleCallback, diff --git a/apps/vben5/packages/@abp/account/src/types/bind.ts b/apps/vben5/packages/@abp/account/src/types/bind.ts new file mode 100644 index 000000000..b85215d3d --- /dev/null +++ b/apps/vben5/packages/@abp/account/src/types/bind.ts @@ -0,0 +1,17 @@ +import type { ButtonType } from 'ant-design-vue/lib/button'; + +interface BindButton { + click: () => Promise | void; + title: string; + type?: ButtonType; +} + +interface BindItem { + buttons?: BindButton[]; + description?: string; + enable?: boolean; + slot?: string; + title: string; +} + +export type { BindItem }; diff --git a/apps/vben5/packages/@abp/account/src/types/external-logins.ts b/apps/vben5/packages/@abp/account/src/types/external-logins.ts new file mode 100644 index 000000000..2d8ce8587 --- /dev/null +++ b/apps/vben5/packages/@abp/account/src/types/external-logins.ts @@ -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, +}; diff --git a/apps/vben5/packages/@abp/account/src/types/index.ts b/apps/vben5/packages/@abp/account/src/types/index.ts index 1dd5f3324..0f182a66b 100644 --- a/apps/vben5/packages/@abp/account/src/types/index.ts +++ b/apps/vben5/packages/@abp/account/src/types/index.ts @@ -1,4 +1,6 @@ export * from './account'; +export * from './bind'; +export * from './external-logins'; export * from './profile'; export * from './token'; export * from './user'; diff --git a/apps/vben5/packages/@abp/account/src/types/token.ts b/apps/vben5/packages/@abp/account/src/types/token.ts index 78df62c18..653b5cde8 100644 --- a/apps/vben5/packages/@abp/account/src/types/token.ts +++ b/apps/vben5/packages/@abp/account/src/types/token.ts @@ -16,6 +16,7 @@ interface PasswordTokenRequest extends TokenRequest { } /** 手机号授权请求数据模型 */ interface PhoneNumberTokenRequest { + [key: string]: any; /** 验证码 */ code: string; /** 手机号 */ @@ -23,6 +24,7 @@ interface PhoneNumberTokenRequest { } /** 扫码登录授权请求数据模型 */ interface QrCodeTokenRequest { + [key: string]: any; /** 二维码Key */ key: string; /** 租户Id */ @@ -30,11 +32,19 @@ interface QrCodeTokenRequest { } /** 用户密码授权请求数据模型 */ interface PasswordTokenRequestModel { + [key: string]: any; /** 用户密码 */ password: string; /** 用户名 */ username: string; } +/** 令牌撤销请求数据类型 */ +interface RevokeTokenRequest { + /** 令牌 */ + token: string; + /** 令牌类型 */ + tokenType?: 'access_token' | 'refresh_token'; +} /** 令牌返回数据模型 */ interface TokenResult { /** 访问令牌 */ @@ -89,6 +99,7 @@ export type { PasswordTokenRequestModel, PhoneNumberTokenRequest, QrCodeTokenRequest, + RevokeTokenRequest, ShouldChangePasswordError, TokenRequest, TokenResult, diff --git a/apps/vben5/packages/@abp/account/src/utils/auth.ts b/apps/vben5/packages/@abp/account/src/utils/auth.ts index 30a7d616f..39260906b 100644 --- a/apps/vben5/packages/@abp/account/src/utils/auth.ts +++ b/apps/vben5/packages/@abp/account/src/utils/auth.ts @@ -1,8 +1,137 @@ +import type { Logger, UserManagerSettings } from 'oidc-client-ts'; + +import type { + PasswordTokenRequestModel, + PhoneNumberTokenRequest, + QrCodeTokenRequest, +} from '../types/token'; + import { useAppConfig } from '@vben/hooks'; -import { UserManager, WebStorageStateStore } from 'oidc-client-ts'; +import { useRequest } from '@abp/request'; +import { + SigninResponse, + UserManager, + WebStorageStateStore, +} from 'oidc-client-ts'; import SecureLS from 'secure-ls'; +class AbpUserManager extends UserManager { + async _fetchUser(logger: Logger, body: URLSearchParams) { + const { request } = useRequest(); + const url = await this.metadataService.getTokenEndpoint(false); + if (!this.settings.omitScopeWhenRequesting) { + body.set('scope', this.settings.scope); + } + const resp = await request(url, { + data: body, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + logger.debug('got signin response'); + const response = new SigninResponse(new URLSearchParams()); + Object.assign(response, resp); + const user = await this._buildUser(response); + if (user.profile && user.profile.sub) { + logger.info('success, signed in subject', user.profile.sub); + } else { + logger.info('no subject'); + } + return user; + } + + _writeChangePasswordToken( + params: URLSearchParams, + model: Record, + ) { + if (model.ChangePasswordToken) { + params.set('ChangePasswordToken', model.ChangePasswordToken); + } + if (model.NewPassword) { + params.set('NewPassword', model.NewPassword); + } + } + _writeTenantId(params: URLSearchParams, model: Record) { + if (model.tenantId) { + params.set('tenantId', model.tenantId); + } + } + _writeTwoFactorToken(params: URLSearchParams, model: Record) { + if (model.TwoFactorProvider) { + params.set('TwoFactorProvider', model.TwoFactorProvider); + } + if (model.TwoFactorCode) { + params.set('TwoFactorCode', model.TwoFactorCode); + } + } + _writeUserId(params: URLSearchParams, model: Record) { + if (model.userId) { + params.set('userId', model.userId); + } + } + async signinQrCode(params: QrCodeTokenRequest) { + const logger = this._logger.create('signinQrCode'); + const client_secret = this.settings.client_secret; + if (!client_secret) { + logger.error('A client_id is required'); + throw new Error('A client_id is required'); + } + const body = new URLSearchParams({ + key: params.key, + grant_type: 'qr_code', + client_id: this.settings.client_id, + client_secret, + }); + this._writeUserId(body, params); + this._writeTenantId(body, params); + this._writeTwoFactorToken(body, params); + return await this._fetchUser(logger, body); + } + + override async signinResourceOwnerCredentials( + params: PasswordTokenRequestModel, + ) { + const logger = this._logger.create('signinResourceOwnerCredentials'); + const client_secret = this.settings.client_secret; + if (!client_secret) { + logger.error('A client_id is required'); + throw new Error('A client_id is required'); + } + const body = new URLSearchParams({ + username: params.username, + password: params.password, + grant_type: 'password', + client_id: this.settings.client_id, + client_secret, + }); + this._writeUserId(body, params); + this._writeTwoFactorToken(body, params); + this._writeChangePasswordToken(body, params); + return await this._fetchUser(logger, body); + } + + async signinSmsCode(params: PhoneNumberTokenRequest) { + const logger = this._logger.create('signinSmsCode'); + const client_secret = this.settings.client_secret; + if (!client_secret) { + logger.error('A client_id is required'); + throw new Error('A client_id is required'); + } + const body = new URLSearchParams({ + phone_number: params.phoneNumber, + phone_verify_code: params.code, + grant_type: 'phone_verify', + client_id: this.settings.client_id, + client_secret, + }); + this._writeUserId(body, params); + this._writeTwoFactorToken(body, params); + return await this._fetchUser(logger, body); + } +} + const { authority, audience, clientId, clientSecret, disablePKCE } = useAppConfig(import.meta.env, import.meta.env.PROD); @@ -17,7 +146,7 @@ const ls = new SecureLS({ // @ts-ignore secure-ls does not have a type definition for this metaKey: `${namespace}-secure-oidc`, }); -export const userManager = new UserManager({ +const oidcSettings: UserManagerSettings = { authority, client_id: clientId, client_secret: clientSecret, @@ -50,4 +179,7 @@ export const userManager = new UserManager({ }, }), disablePKCE, -}); +}; +const userManager = new AbpUserManager(oidcSettings); + +export { oidcSettings, userManager }; diff --git a/apps/vben5/packages/@abp/auditing/src/components/audit-logs/AuditLogTable.vue b/apps/vben5/packages/@abp/auditing/src/components/audit-logs/AuditLogTable.vue index cc97d7b5b..5eb7e7e92 100644 --- a/apps/vben5/packages/@abp/auditing/src/components/audit-logs/AuditLogTable.vue +++ b/apps/vben5/packages/@abp/auditing/src/components/audit-logs/AuditLogTable.vue @@ -1,5 +1,4 @@ diff --git a/apps/vben5/packages/@abp/identity/src/components/organization-units/SelectRoleModal.vue b/apps/vben5/packages/@abp/identity/src/components/organization-units/SelectRoleModal.vue index ec5e33f3f..9f34ab20f 100644 --- a/apps/vben5/packages/@abp/identity/src/components/organization-units/SelectRoleModal.vue +++ b/apps/vben5/packages/@abp/identity/src/components/organization-units/SelectRoleModal.vue @@ -68,27 +68,30 @@ const [Modal, modalApi] = useVbenModal({ }); const gridOptions: VxeGridProps = { - checkboxConfig: { - highlight: true, - labelField: 'name', - }, columns: [ + { + align: 'center', + type: 'checkbox', + width: 80, + }, { align: 'left', field: 'name', + sortable: true, title: $t('AbpIdentity.DisplayName:RoleName'), - type: 'checkbox', }, ], exportConfig: {}, keepSource: true, proxyConfig: { ajax: { - query: async ({ page }, formValues) => { + query: async ({ page, sort }, formValues) => { + const sorting = sort.order ? `${sort.field} ${sort.order}` : undefined; const state = modalApi.getData>(); return await getUnaddedRoleListApi({ id: state.id, maxResultCount: page.pageSize, + sorting, skipCount: (page.currentPage - 1) * page.pageSize, ...formValues, }); @@ -100,7 +103,11 @@ const gridOptions: VxeGridProps = { list: 'items', }, }, - toolbarConfig: {}, + toolbarConfig: { + refresh: { + code: 'query', + }, + }, }; const gridEvents: VxeGridListeners = { @@ -110,15 +117,18 @@ const gridEvents: VxeGridListeners = { checkboxChange: (e) => { selectedRoles.value = e.records; }, + sortChange: () => { + gridApi.query(); + }, }; -const [Grid, { query }] = useVbenVxeGrid({ +const [Grid, gridApi] = useVbenVxeGrid({ formOptions, gridEvents, gridOptions, }); function onRefresh() { - nextTick(query); + nextTick(gridApi.query); } diff --git a/apps/vben5/packages/@abp/identity/src/components/roles/RoleTable.vue b/apps/vben5/packages/@abp/identity/src/components/roles/RoleTable.vue index 1bc0d6eaf..85aad1ba7 100644 --- a/apps/vben5/packages/@abp/identity/src/components/roles/RoleTable.vue +++ b/apps/vben5/packages/@abp/identity/src/components/roles/RoleTable.vue @@ -87,6 +87,7 @@ const gridOptions: VxeGridProps = { align: 'left', field: 'name', slots: { default: 'name' }, + sortable: true, title: $t('AbpIdentity.DisplayName:RoleName'), }, { @@ -101,8 +102,10 @@ const gridOptions: VxeGridProps = { keepSource: true, proxyConfig: { ajax: { - query: async ({ page }, formValues) => { + query: async ({ page, sort }, formValues) => { + const sorting = sort.order ? `${sort.field} ${sort.order}` : undefined; return await getPagedListApi({ + sorting, maxResultCount: page.pageSize, skipCount: (page.currentPage - 1) * page.pageSize, ...formValues, @@ -117,19 +120,23 @@ const gridOptions: VxeGridProps = { toolbarConfig: { custom: true, export: true, - // import: true, - refresh: true, + refresh: { + code: 'query', + }, zoom: true, }, }; const gridEvents: VxeGridListeners = { cellClick: () => {}, + sortChange: () => { + gridApi.query(); + }, }; const [RoleEditModal, roleModalApi] = useVbenModal({ connectedComponent: RoleModal, }); -const [Grid, { query }] = useVbenVxeGrid({ +const [Grid, gridApi] = useVbenVxeGrid({ formOptions, gridEvents, gridOptions, @@ -155,7 +162,7 @@ const handleDelete = (row: IdentityRoleDto) => { onOk: async () => { await deleteApi(row.id); message.success($t('AbpUi.DeletedSuccessfully')); - query(); + gridApi.query(); }, title: $t('AbpUi.AreYouSure'), }); @@ -300,8 +307,8 @@ function onPermissionChange(_name: string, key: string) { - query()" /> - + gridApi.query()" /> + gridApi.query()" /> diff --git a/apps/vben5/packages/@abp/identity/src/components/security-logs/SecurityLogTable.vue b/apps/vben5/packages/@abp/identity/src/components/security-logs/SecurityLogTable.vue index fc3d3ae42..ef0910f31 100644 --- a/apps/vben5/packages/@abp/identity/src/components/security-logs/SecurityLogTable.vue +++ b/apps/vben5/packages/@abp/identity/src/components/security-logs/SecurityLogTable.vue @@ -1,5 +1,4 @@ diff --git a/apps/vben5/packages/@abp/identity/src/components/sessions/SessionTable.vue b/apps/vben5/packages/@abp/identity/src/components/sessions/SessionTable.vue index 6830f98f1..249a37b21 100644 --- a/apps/vben5/packages/@abp/identity/src/components/sessions/SessionTable.vue +++ b/apps/vben5/packages/@abp/identity/src/components/sessions/SessionTable.vue @@ -82,6 +82,7 @@ const gridOptions: VxeGridProps = { align: 'left', field: 'sessionId', minWidth: 150, + sortable: true, title: $t('AbpIdentity.DisplayName:SessionId'), }, { @@ -89,11 +90,13 @@ const gridOptions: VxeGridProps = { field: 'device', minWidth: 120, slots: { default: 'device' }, + sortable: true, title: $t('AbpIdentity.DisplayName:Device'), }, { align: 'left', field: 'deviceInfo', + sortable: true, title: $t('AbpIdentity.DisplayName:DeviceInfo'), width: 'auto', }, @@ -101,24 +104,28 @@ const gridOptions: VxeGridProps = { align: 'left', field: 'clientId', minWidth: 120, + sortable: true, title: $t('AbpIdentity.DisplayName:ClientId'), }, { align: 'left', field: 'ipAddresses', minWidth: 120, + sortable: true, title: $t('AbpIdentity.DisplayName:IpAddresses'), }, { align: 'left', field: 'signedIn', minWidth: 120, + sortable: true, title: $t('AbpIdentity.DisplayName:SignedIn'), }, { align: 'left', field: 'lastAccessed', minWidth: 120, + sortable: true, title: $t('AbpIdentity.DisplayName:LastAccessed'), }, { @@ -137,8 +144,10 @@ const gridOptions: VxeGridProps = { keepSource: true, proxyConfig: { ajax: { - query: async ({ page }, formValues) => { + query: async ({ page, sort }, formValues) => { + const sorting = sort.order ? `${sort.field} ${sort.order}` : undefined; return await getSessionsApi({ + sorting, maxResultCount: page.pageSize, skipCount: (page.currentPage - 1) * page.pageSize, ...formValues, @@ -153,14 +162,18 @@ const gridOptions: VxeGridProps = { toolbarConfig: { custom: true, export: true, - // import: true, - refresh: true, + refresh: { + code: 'query', + }, zoom: true, }, }; const gridEvents: VxeGridListeners = { cellClick: () => {}, + sortChange: () => { + gridApi.query(); + }, }; const [Grid, gridApi] = useVbenVxeGrid({ diff --git a/apps/vben5/packages/@abp/identity/src/components/sessions/UserSessionTable.vue b/apps/vben5/packages/@abp/identity/src/components/sessions/UserSessionTable.vue index b2c02d0b7..aa0c99a7f 100644 --- a/apps/vben5/packages/@abp/identity/src/components/sessions/UserSessionTable.vue +++ b/apps/vben5/packages/@abp/identity/src/components/sessions/UserSessionTable.vue @@ -24,17 +24,20 @@ const DescriptionItem = Descriptions.Item; const { hasAccessByCodes } = useAccess(); const abpStore = useAbpStore(); -/** 获取登录用户会话Id */ -const getMySessionId = computed(() => { - return abpStore.application?.currentUser.sessionId; +/** 获取登录用户 */ +const getCurrentUser = computed(() => { + return abpStore.application?.currentUser; }); /** 获取是否允许撤销会话 */ const getAllowRevokeSession = computed(() => { return (session: IdentitySessionDto) => { - if (getMySessionId.value === session.sessionId) { + if (getCurrentUser.value?.sessionId === session.sessionId) { return false; } - return hasAccessByCodes([IdentitySessionPermissions.Revoke]); + return ( + getCurrentUser.value?.id === session.userId || + hasAccessByCodes([IdentitySessionPermissions.Revoke]) + ); }; }); @@ -106,7 +109,10 @@ function onDelete(session: IdentitySessionDto) { {{ row.device }} - + {{ $t('AbpIdentity.CurrentSession') }} diff --git a/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue b/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue index 7d68dcf17..520e3d659 100644 --- a/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue +++ b/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue @@ -96,11 +96,13 @@ const gridOptions: VxeGridProps = { { field: 'isActive', slots: { default: 'active' }, + sortable: true, title: $t('AbpIdentity.DisplayName:IsActive'), }, { field: 'userName', minWidth: '100px', + sortable: true, title: $t('AbpIdentity.DisplayName:UserName'), }, { @@ -108,14 +110,24 @@ const gridOptions: VxeGridProps = { field: 'email', minWidth: '120px', slots: { default: 'email' }, + sortable: true, title: $t('AbpIdentity.DisplayName:Email'), }, - { field: 'surname', title: $t('AbpIdentity.DisplayName:Surname') }, - { field: 'name', title: $t('AbpIdentity.DisplayName:Name') }, + { + field: 'surname', + sortable: true, + title: $t('AbpIdentity.DisplayName:Surname'), + }, + { + field: 'name', + sortable: true, + title: $t('AbpIdentity.DisplayName:Name'), + }, { align: 'left', field: 'phoneNumber', slots: { default: 'phoneNumber' }, + sortable: true, title: $t('AbpIdentity.DisplayName:PhoneNumber'), }, { @@ -123,6 +135,7 @@ const gridOptions: VxeGridProps = { formatter: ({ cellValue }) => { return cellValue ? formatToDateTime(cellValue) : ''; }, + sortable: true, title: $t('AbpIdentity.LockoutEnd'), }, { @@ -137,8 +150,10 @@ const gridOptions: VxeGridProps = { keepSource: true, proxyConfig: { ajax: { - query: async ({ page }, formValues) => { + query: async ({ page, sort }, formValues) => { + const sorting = sort.order ? `${sort.field} ${sort.order}` : undefined; return await getPagedListApi({ + sorting, maxResultCount: page.pageSize, skipCount: (page.currentPage - 1) * page.pageSize, ...formValues, @@ -153,14 +168,18 @@ const gridOptions: VxeGridProps = { toolbarConfig: { custom: true, export: true, - // import: true, - refresh: true, + refresh: { + code: 'query', + }, zoom: true, }, }; const gridEvents: VxeGridListeners = { cellClick: () => {}, + sortChange: () => { + gridApi.query(); + }, }; const [UserEditModal, userModalApi] = useVbenModal({ connectedComponent: UserModal, @@ -188,7 +207,7 @@ const [UserSessionDrawer, userSessionDrawerApi] = useVbenDrawer({ () => import('./UserSessionDrawer.vue'), ), }); -const [Grid, { query }] = useVbenVxeGrid({ +const [Grid, gridApi] = useVbenVxeGrid({ formOptions, gridEvents, gridOptions, @@ -214,7 +233,7 @@ const handleDelete = (row: IdentityUserDto) => { onOk: async () => { await deleteApi(row.id); message.success($t('AbpUi.DeletedSuccessfully')); - query(); + await gridApi.query(); }, title: $t('AbpUi.AreYouSure'), }); @@ -222,7 +241,7 @@ const handleDelete = (row: IdentityUserDto) => { const handleUnlock = async (row: IdentityUserDto) => { await unLockApi(row.id); - await query(); + await gridApi.query(); }; const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => { @@ -436,10 +455,10 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => { - - - query()" /> - + gridApi.query()" /> + gridApi.query()" /> + gridApi.query()" /> + gridApi.query()" /> diff --git a/apps/vben5/packages/@abp/localization/src/components/languages/LocalizationLanguageTable.vue b/apps/vben5/packages/@abp/localization/src/components/languages/LocalizationLanguageTable.vue index e70fc11fe..5e1620476 100644 --- a/apps/vben5/packages/@abp/localization/src/components/languages/LocalizationLanguageTable.vue +++ b/apps/vben5/packages/@abp/localization/src/components/languages/LocalizationLanguageTable.vue @@ -5,12 +5,12 @@ import type { VbenFormProps } from '@vben/common-ui'; import type { LanguageDto } from '../../types/languages'; -import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue'; +import { defineAsyncComponent, h, onMounted, ref } from 'vue'; import { useVbenModal } from '@vben/common-ui'; import { $t } from '@vben/locales'; -import { useAbpStore } from '@abp/core'; +import { sortby, useAbpStore } from '@abp/core'; import { useVbenVxeGrid } from '@abp/ui'; import { DeleteOutlined, @@ -28,11 +28,6 @@ defineOptions({ }); const dataSource = ref([]); -const pageState = reactive({ - current: 1, - size: 10, - total: 0, -}); const abpStore = useAbpStore(); const { deleteApi, getListApi } = useLanguagesApi(); @@ -43,7 +38,6 @@ const formOptions: VbenFormProps = { collapsed: false, handleReset: onReset, async handleSubmit(params) { - pageState.current = 1; await onGet(params); }, schema: [ @@ -66,18 +60,21 @@ const gridOptions: VxeGridProps = { align: 'left', field: 'cultureName', minWidth: 150, + sortable: true, title: $t('AbpLocalization.DisplayName:CultureName'), }, { align: 'left', field: 'displayName', minWidth: 150, + sortable: true, title: $t('AbpLocalization.DisplayName:DisplayName'), }, { align: 'left', field: 'uiCultureName', minWidth: 150, + sortable: true, title: $t('AbpLocalization.DisplayName:UiCultureName'), }, { @@ -90,6 +87,30 @@ const gridOptions: VxeGridProps = { ], exportConfig: {}, keepSource: true, + proxyConfig: { + ajax: { + query: async ({ page, sort }) => { + let items = sortby(dataSource.value, sort.field); + if (sort.order === 'desc') { + items = items.reverse(); + } + const result = { + totalCount: dataSource.value.length, + items: items.slice( + (page.currentPage - 1) * page.pageSize, + page.currentPage * page.pageSize, + ), + }; + return new Promise((resolve) => { + resolve(result); + }); + }, + }, + response: { + total: 'totalCount', + list: 'items', + }, + }, toolbarConfig: { custom: true, export: true, @@ -99,10 +120,8 @@ const gridOptions: VxeGridProps = { }; const gridEvents: VxeGridListeners = { - pageChange(params) { - pageState.current = params.currentPage; - pageState.size = params.pageSize; - onPageChange(); + sortChange: () => { + gridApi.query(); }, }; @@ -122,9 +141,8 @@ async function onGet(input?: Record) { try { gridApi.setLoading(true); const { items } = await getListApi(input); - pageState.total = items.length; dataSource.value = items; - onPageChange(); + setTimeout(() => gridApi.reload(), 100); } finally { gridApi.setLoading(false); } @@ -136,21 +154,6 @@ async function onReset() { await onGet(input); } -function onPageChange() { - const items = dataSource.value.slice( - (pageState.current - 1) * pageState.size, - pageState.current * pageState.size, - ); - gridApi.setGridOptions({ - data: items, - pagerConfig: { - currentPage: pageState.current, - pageSize: pageState.size, - total: pageState.total, - }, - }); -} - function onCreate() { modalApi.setData({}); modalApi.open(); diff --git a/apps/vben5/packages/@abp/localization/src/components/resources/LocalizationResourceTable.vue b/apps/vben5/packages/@abp/localization/src/components/resources/LocalizationResourceTable.vue index 3a1c6f7d0..17588471e 100644 --- a/apps/vben5/packages/@abp/localization/src/components/resources/LocalizationResourceTable.vue +++ b/apps/vben5/packages/@abp/localization/src/components/resources/LocalizationResourceTable.vue @@ -5,12 +5,12 @@ import type { VbenFormProps } from '@vben/common-ui'; import type { ResourceDto } from '../../types/resources'; -import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue'; +import { defineAsyncComponent, h, onMounted, ref } from 'vue'; import { useVbenModal } from '@vben/common-ui'; import { $t } from '@vben/locales'; -import { useAbpStore } from '@abp/core'; +import { sortby, useAbpStore } from '@abp/core'; import { useVbenVxeGrid } from '@abp/ui'; import { DeleteOutlined, @@ -28,12 +28,6 @@ defineOptions({ }); const dataSource = ref([]); -const pageState = reactive({ - current: 1, - size: 10, - total: 0, -}); - const abpStore = useAbpStore(); const { deleteApi, getListApi } = useResourcesApi(); const { getLocalizationApi } = useLocalizationsApi(); @@ -43,7 +37,6 @@ const formOptions: VbenFormProps = { collapsed: false, handleReset: onReset, async handleSubmit(params) { - pageState.current = 1; await onGet(params); }, schema: [ @@ -66,12 +59,14 @@ const gridOptions: VxeGridProps = { align: 'left', field: 'name', minWidth: 150, + sortable: true, title: $t('AbpFeatureManagement.DisplayName:Name'), }, { align: 'left', field: 'displayName', minWidth: 150, + sortable: true, title: $t('AbpFeatureManagement.DisplayName:DisplayName'), }, { @@ -84,6 +79,30 @@ const gridOptions: VxeGridProps = { ], exportConfig: {}, keepSource: true, + proxyConfig: { + ajax: { + query: async ({ page, sort }) => { + let items = sortby(dataSource.value, sort.field); + if (sort.order === 'desc') { + items = items.reverse(); + } + const result = { + totalCount: dataSource.value.length, + items: items.slice( + (page.currentPage - 1) * page.pageSize, + page.currentPage * page.pageSize, + ), + }; + return new Promise((resolve) => { + resolve(result); + }); + }, + }, + response: { + total: 'totalCount', + list: 'items', + }, + }, toolbarConfig: { custom: true, export: true, @@ -93,10 +112,8 @@ const gridOptions: VxeGridProps = { }; const gridEvents: VxeGridListeners = { - pageChange(params) { - pageState.current = params.currentPage; - pageState.size = params.pageSize; - onPageChange(); + sortChange: () => { + gridApi.query(); }, }; @@ -116,9 +133,8 @@ async function onGet(input?: Record) { try { gridApi.setLoading(true); const { items } = await getListApi(input); - pageState.total = items.length; dataSource.value = items; - onPageChange(); + setTimeout(() => gridApi.reload(), 100); } finally { gridApi.setLoading(false); } @@ -130,21 +146,6 @@ async function onReset() { await onGet(input); } -function onPageChange() { - const items = dataSource.value.slice( - (pageState.current - 1) * pageState.size, - pageState.current * pageState.size, - ); - gridApi.setGridOptions({ - data: items, - pagerConfig: { - currentPage: pageState.current, - pageSize: pageState.size, - total: pageState.total, - }, - }); -} - function onCreate() { modalApi.setData({}); modalApi.open(); diff --git a/apps/vben5/packages/@abp/localization/src/components/texts/LocalizationTextTable.vue b/apps/vben5/packages/@abp/localization/src/components/texts/LocalizationTextTable.vue index 4b0dada4e..dac88ef0c 100644 --- a/apps/vben5/packages/@abp/localization/src/components/texts/LocalizationTextTable.vue +++ b/apps/vben5/packages/@abp/localization/src/components/texts/LocalizationTextTable.vue @@ -11,7 +11,7 @@ import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue'; import { useVbenModal } from '@vben/common-ui'; import { $t } from '@vben/locales'; -import { useAbpStore } from '@abp/core'; +import { sortby, useAbpStore } from '@abp/core'; import { useVbenVxeGrid } from '@abp/ui'; import { EditOutlined, PlusOutlined } from '@ant-design/icons-vue'; import { Button, Select } from 'ant-design-vue'; @@ -39,11 +39,6 @@ const targetValueOptions = reactive([ value: 'true', }, ]); -const pageState = reactive({ - current: 1, - size: 10, - total: 0, -}); const abpStore = useAbpStore(); const { getListApi } = useTextsApi(); @@ -61,10 +56,8 @@ const formOptions: VbenFormProps = { class: 'w-full', }, }, - compact: false, handleReset: onReset, async handleSubmit(params) { - pageState.current = 1; await onGet(params); }, schema: [ @@ -111,24 +104,28 @@ const gridOptions: VxeGridProps = { align: 'left', field: 'key', minWidth: 150, + sortable: true, title: $t('AbpLocalization.DisplayName:Key'), }, { align: 'left', field: 'value', minWidth: 150, + sortable: true, title: $t('AbpLocalization.DisplayName:Value'), }, { align: 'left', field: 'targetValue', minWidth: 150, + sortable: true, title: $t('AbpLocalization.DisplayName:TargetValue'), }, { align: 'left', field: 'resourceName', minWidth: 150, + sortable: true, title: $t('AbpLocalization.DisplayName:ResourceName'), }, { @@ -141,6 +138,30 @@ const gridOptions: VxeGridProps = { ], exportConfig: {}, keepSource: true, + proxyConfig: { + ajax: { + query: async ({ page, sort }) => { + let items = sortby(dataSource.value, sort.field); + if (sort.order === 'desc') { + items = items.reverse(); + } + const result = { + totalCount: dataSource.value.length, + items: items.slice( + (page.currentPage - 1) * page.pageSize, + page.currentPage * page.pageSize, + ), + }; + return new Promise((resolve) => { + resolve(result); + }); + }, + }, + response: { + total: 'totalCount', + list: 'items', + }, + }, toolbarConfig: { custom: true, export: true, @@ -150,10 +171,8 @@ const gridOptions: VxeGridProps = { }; const gridEvents: VxeGridListeners = { - pageChange(params) { - pageState.current = params.currentPage; - pageState.size = params.pageSize; - onPageChange(); + sortChange: () => { + gridApi.query(); }, }; @@ -186,9 +205,8 @@ async function onGet(input: Record) { try { gridApi.setLoading(true); const { items } = await getListApi(input as any); - pageState.total = items.length; dataSource.value = items; - onPageChange(); + setTimeout(() => gridApi.reload(), 100); } finally { gridApi.setLoading(false); } @@ -200,21 +218,6 @@ async function onReset() { await onGet(input); } -function onPageChange() { - const items = dataSource.value.slice( - (pageState.current - 1) * pageState.size, - pageState.current * pageState.size, - ); - gridApi.setGridOptions({ - data: items, - pagerConfig: { - currentPage: pageState.current, - pageSize: pageState.size, - total: pageState.total, - }, - }); -} - async function onCreate() { const input = await gridApi.formApi.getValues(); modalApi.setData({ diff --git a/apps/vben5/packages/@abp/notifications/src/components/definitions/groups/NotificationGroupDefinitionTable.vue b/apps/vben5/packages/@abp/notifications/src/components/definitions/groups/NotificationGroupDefinitionTable.vue index 67abc62ee..06e245ec9 100644 --- a/apps/vben5/packages/@abp/notifications/src/components/definitions/groups/NotificationGroupDefinitionTable.vue +++ b/apps/vben5/packages/@abp/notifications/src/components/definitions/groups/NotificationGroupDefinitionTable.vue @@ -6,14 +6,14 @@ import type { VbenFormProps } from '@vben/common-ui'; import type { NotificationGroupDefinitionDto } from '../../../types/groups'; -import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue'; +import { defineAsyncComponent, h, onMounted, ref } from 'vue'; import { useAccess } from '@vben/access'; import { useVbenModal } from '@vben/common-ui'; import { createIconifyIcon } from '@vben/icons'; import { $t } from '@vben/locales'; -import { useLocalization, useLocalizationSerializer } from '@abp/core'; +import { sortby, useLocalization, useLocalizationSerializer } from '@abp/core'; import { useVbenVxeGrid } from '@abp/ui'; import { DeleteOutlined, @@ -37,11 +37,6 @@ const MenuItem = Menu.Item; const DefinitionIcon = createIconifyIcon('nimbus:notification'); const dataSource = ref([]); -const pageState = reactive({ - current: 1, - size: 10, - total: 0, -}); const { Lr } = useLocalization(); const { hasAccessByCodes } = useAccess(); @@ -53,7 +48,6 @@ const formOptions: VbenFormProps = { collapsed: false, handleReset: onReset, async handleSubmit(params) { - pageState.current = 1; await onGet(params); }, schema: [ @@ -76,12 +70,14 @@ const gridOptions: VxeGridProps = { align: 'left', field: 'name', minWidth: 150, + sortable: true, title: $t('Notifications.DisplayName:Name'), }, { align: 'left', field: 'displayName', minWidth: 150, + sortable: true, title: $t('Notifications.DisplayName:DisplayName'), }, { @@ -94,6 +90,30 @@ const gridOptions: VxeGridProps = { ], exportConfig: {}, keepSource: true, + proxyConfig: { + ajax: { + query: async ({ page, sort }) => { + let items = sortby(dataSource.value, sort.field); + if (sort.order === 'desc') { + items = items.reverse(); + } + const result = { + totalCount: dataSource.value.length, + items: items.slice( + (page.currentPage - 1) * page.pageSize, + page.currentPage * page.pageSize, + ), + }; + return new Promise((resolve) => { + resolve(result); + }); + }, + }, + response: { + total: 'totalCount', + list: 'items', + }, + }, toolbarConfig: { custom: true, export: true, @@ -103,10 +123,8 @@ const gridOptions: VxeGridProps = { }; const gridEvents: VxeGridListeners = { - pageChange(params) { - pageState.current = params.currentPage; - pageState.size = params.pageSize; - onPageChange(); + sortChange: () => { + gridApi.query(); }, }; @@ -131,7 +149,6 @@ async function onGet(input?: Record) { try { gridApi.setLoading(true); const { items } = await getListApi(input); - pageState.total = items.length; dataSource.value = items.map((item) => { const localizableString = deserialize(item.displayName); return { @@ -139,7 +156,7 @@ async function onGet(input?: Record) { displayName: Lr(localizableString.resourceName, localizableString.name), }; }); - onPageChange(); + setTimeout(() => gridApi.reload(), 100); } finally { gridApi.setLoading(false); } @@ -151,21 +168,6 @@ async function onReset() { await onGet(input); } -function onPageChange() { - const items = dataSource.value.slice( - (pageState.current - 1) * pageState.size, - pageState.current * pageState.size, - ); - gridApi.setGridOptions({ - data: items, - pagerConfig: { - currentPage: pageState.current, - pageSize: pageState.size, - total: pageState.total, - }, - }); -} - function onCreate() { groupModalApi.setData({}); groupModalApi.open(); diff --git a/apps/vben5/packages/@abp/notifications/src/components/definitions/notifications/NotificationDefinitionTable.vue b/apps/vben5/packages/@abp/notifications/src/components/definitions/notifications/NotificationDefinitionTable.vue index 96762869f..dc49327ba 100644 --- a/apps/vben5/packages/@abp/notifications/src/components/definitions/notifications/NotificationDefinitionTable.vue +++ b/apps/vben5/packages/@abp/notifications/src/components/definitions/notifications/NotificationDefinitionTable.vue @@ -7,7 +7,7 @@ import type { VbenFormProps } from '@vben/common-ui'; import type { NotificationDefinitionDto } from '../../../types/definitions'; import type { NotificationGroupDefinitionDto } from '../../../types/groups'; -import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue'; +import { defineAsyncComponent, h, onMounted, ref } from 'vue'; import { useAccess } from '@vben/access'; import { useVbenModal } from '@vben/common-ui'; @@ -16,6 +16,7 @@ import { $t } from '@vben/locales'; import { listToTree, + sortby, useLocalization, useLocalizationSerializer, } from '@abp/core'; @@ -79,18 +80,12 @@ const { deleteApi, getListApi: getDefinitionsApi } = useNotificationDefinitionsApi(); const definitionGroups = ref([]); -const pageState = reactive({ - current: 1, - size: 10, - total: 0, -}); const formOptions: VbenFormProps = { // 默认展开 collapsed: false, handleReset: onReset, async handleSubmit(params) { - pageState.current = 1; await onGet(params); }, schema: [ @@ -125,12 +120,14 @@ const gridOptions: VxeGridProps = { align: 'left', field: 'name', minWidth: 150, + sortable: true, title: $t('Notifications.DisplayName:Name'), }, { align: 'left', field: 'displayName', minWidth: 150, + sortable: true, title: $t('Notifications.DisplayName:DisplayName'), }, ], @@ -140,6 +137,30 @@ const gridOptions: VxeGridProps = { }, exportConfig: {}, keepSource: true, + proxyConfig: { + ajax: { + query: async ({ page, sort }) => { + let items = sortby(definitionGroups.value, sort.field); + if (sort.order === 'desc') { + items = items.reverse(); + } + const result = { + totalCount: definitionGroups.value.length, + items: items.slice( + (page.currentPage - 1) * page.pageSize, + page.currentPage * page.pageSize, + ), + }; + return new Promise((resolve) => { + resolve(result); + }); + }, + }, + response: { + total: 'totalCount', + list: 'items', + }, + }, toolbarConfig: { custom: true, export: true, @@ -157,6 +178,8 @@ const subGridColumns: VxeGridProps['columns'] = [ align: 'left', field: 'name', minWidth: 150, + resizable: true, + sortable: true, title: $t('Notifications.DisplayName:Name'), treeNode: true, }, @@ -164,12 +187,16 @@ const subGridColumns: VxeGridProps['columns'] = [ align: 'left', field: 'displayName', minWidth: 120, + resizable: true, + sortable: true, title: $t('Notifications.DisplayName:DisplayName'), }, { align: 'left', field: 'description', minWidth: 120, + resizable: true, + sortable: true, title: $t('Notifications.DisplayName:Description'), }, { @@ -177,37 +204,49 @@ const subGridColumns: VxeGridProps['columns'] = [ field: 'allowSubscriptionToClients', minWidth: 120, slots: { default: 'allowSubscriptionToClients' }, + resizable: true, + sortable: true, title: $t('Notifications.DisplayName:AllowSubscriptionToClients'), }, { align: 'left', field: 'template', minWidth: 150, + resizable: true, + sortable: true, title: $t('Notifications.DisplayName:Template'), }, { align: 'left', field: 'notificationLifetime', minWidth: 150, + resizable: true, + sortable: true, title: $t('Notifications.DisplayName:NotificationLifetime'), }, { align: 'left', field: 'notificationType', minWidth: 150, + resizable: true, + sortable: true, title: $t('Notifications.DisplayName:NotificationType'), }, { align: 'left', field: 'contentType', minWidth: 150, + resizable: true, + sortable: true, title: $t('Notifications.DisplayName:ContentType'), }, { align: 'left', field: 'providers', minWidth: 150, + resizable: true, slots: { default: 'providers' }, + sortable: true, title: $t('Notifications.DisplayName:Providers'), }, { @@ -220,10 +259,8 @@ const subGridColumns: VxeGridProps['columns'] = [ ]; const gridEvents: VxeGridListeners = { - pageChange(params) { - pageState.current = params.currentPage; - pageState.size = params.pageSize; - onPageChange(); + sortChange: () => { + gridApi.query(); }, }; @@ -249,7 +286,6 @@ async function onGet(input?: Record) { gridApi.setLoading(true); const groupRes = await getGroupsApi(input); const definitionRes = await getDefinitionsApi(input); - pageState.total = groupRes.items.length; definitionGroups.value = groupRes.items.map((group) => { const localizableGroup = deserialize(group.displayName); const definitions = definitionRes.items @@ -276,7 +312,7 @@ async function onGet(input?: Record) { }), }; }); - onPageChange(); + setTimeout(() => gridApi.reload(), 100); } finally { gridApi.setLoading(false); } @@ -288,21 +324,6 @@ async function onReset() { await onGet(input); } -function onPageChange() { - const items = definitionGroups.value.slice( - (pageState.current - 1) * pageState.size, - pageState.current * pageState.size, - ); - gridApi.setGridOptions({ - data: items, - pagerConfig: { - currentPage: pageState.current, - pageSize: pageState.size, - total: pageState.total, - }, - }); -} - function onCreate() { modalApi.setData({}); modalApi.open(); diff --git a/apps/vben5/packages/@abp/notifications/src/components/my-notifilers/MyNotificationTable.vue b/apps/vben5/packages/@abp/notifications/src/components/my-notifilers/MyNotificationTable.vue index e59bb8a3e..7d824f144 100644 --- a/apps/vben5/packages/@abp/notifications/src/components/my-notifilers/MyNotificationTable.vue +++ b/apps/vben5/packages/@abp/notifications/src/components/my-notifilers/MyNotificationTable.vue @@ -107,6 +107,7 @@ const gridOptions: VxeGridProps = { } }, minWidth: 50, + sortable: true, title: $t('Notifications.Notifications:Type'), }, { @@ -116,6 +117,7 @@ const gridOptions: VxeGridProps = { return cellValue ? formatToDateTime(cellValue) : ''; }, minWidth: 120, + sortable: true, title: $t('Notifications.Notifications:SendTime'), }, { @@ -144,8 +146,10 @@ const gridOptions: VxeGridProps = { keepSource: true, proxyConfig: { ajax: { - query: async ({ page }, formValues) => { + query: async ({ page, sort }, formValues) => { + const sorting = sort.order ? `${sort.field} ${sort.order}` : undefined; const { totalCount, items } = await getMyNotifilersApi({ + sorting, maxResultCount: page.pageSize, skipCount: (page.currentPage - 1) * page.pageSize, ...formValues, @@ -171,8 +175,9 @@ const gridOptions: VxeGridProps = { toolbarConfig: { custom: true, export: true, - // import: true, - refresh: true, + refresh: { + code: 'query', + }, zoom: true, }, }; @@ -184,6 +189,9 @@ const gridEvents: VxeGridListeners = { checkboxChange: (params) => { selectedKeys.value = params.records.map((record) => record.id); }, + sortChange: () => { + gridApi.query(); + }, }; const [Grid, gridApi] = useVbenVxeGrid({ diff --git a/apps/vben5/packages/@abp/openiddict/src/components/applications/ApplicationTable.vue b/apps/vben5/packages/@abp/openiddict/src/components/applications/ApplicationTable.vue index 9c32c2566..76d5f5bec 100644 --- a/apps/vben5/packages/@abp/openiddict/src/components/applications/ApplicationTable.vue +++ b/apps/vben5/packages/@abp/openiddict/src/components/applications/ApplicationTable.vue @@ -1,5 +1,5 @@ + + + + + + + + + + + + + + + {{ $t('workbench.header.notifier.title') }} + + {{ + $t('workbench.header.notifier.count', [notifierCount]) + }} + + + + diff --git a/apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchQuickNav.vue b/apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchQuickNav.vue new file mode 100644 index 000000000..8479356db --- /dev/null +++ b/apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchQuickNav.vue @@ -0,0 +1,124 @@ + + + + + + {{ title }} + + + + + + + {{ item.displayName }} + + + onMenuClick(menuKey.toString(), item) + " + > + + {{ $t('workbench.content.favoriteMenu.delete') }} + + + + + + + + + + diff --git a/apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchQuickNavModal.vue b/apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchQuickNavModal.vue new file mode 100644 index 000000000..3bd56cc3e --- /dev/null +++ b/apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchQuickNavModal.vue @@ -0,0 +1,137 @@ + + + + + + + + + ({{ slotProps.value }}) + + + + + + + + {{ item.displayName }} + + + + + + + + + diff --git a/apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchTodo.vue b/apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchTodo.vue new file mode 100644 index 000000000..8751f8b79 --- /dev/null +++ b/apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchTodo.vue @@ -0,0 +1,64 @@ + + + + + + {{ title }} + + + + + + + + + + {{ item.title }} + + + + + + + + {{ item.date }} + + + + + + + diff --git a/apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchTrends.vue b/apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchTrends.vue new file mode 100644 index 000000000..48267cdb4 --- /dev/null +++ b/apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchTrends.vue @@ -0,0 +1,65 @@ + + + + + + {{ title }} + + + + + + + + + + {{ item.title }} + + + + + + + + {{ item.date }} + + + + + + + diff --git a/apps/vben5/packages/@abp/platform/src/components/workbench/index.vue b/apps/vben5/packages/@abp/platform/src/components/workbench/index.vue new file mode 100644 index 000000000..d531d34c2 --- /dev/null +++ b/apps/vben5/packages/@abp/platform/src/components/workbench/index.vue @@ -0,0 +1,218 @@ + + + + + + + {{ getWelcomeTitle }} + + 今日晴,20℃ - 32℃! + + + + + $emit('navTo', menu)" + /> + + + + + + + + + + + + + + + + + + + diff --git a/apps/vben5/packages/@abp/platform/src/components/workbench/types.ts b/apps/vben5/packages/@abp/platform/src/components/workbench/types.ts new file mode 100644 index 000000000..13dea780f --- /dev/null +++ b/apps/vben5/packages/@abp/platform/src/components/workbench/types.ts @@ -0,0 +1,10 @@ +interface FavoriteMenu { + color?: string; + displayName: string; + icon?: string; + id: string; + isDefault: boolean; + path?: string; +} + +export type { FavoriteMenu }; diff --git a/apps/vben5/packages/@abp/platform/src/hooks/useMenuTransform.ts b/apps/vben5/packages/@abp/platform/src/hooks/useMenuTransform.ts index a7fdb9177..8c6ffc0b3 100644 --- a/apps/vben5/packages/@abp/platform/src/hooks/useMenuTransform.ts +++ b/apps/vben5/packages/@abp/platform/src/hooks/useMenuTransform.ts @@ -2,9 +2,12 @@ import type { RouteRecordStringComponent } from '@vben/types'; import type { MenuDto } from '../types'; +import { useUserStore } from '@vben/stores'; + import { listToTree } from '@abp/core'; export function useMenuTransform() { + const userStore = useUserStore(); function mapMetaString(meta: Record, key: string) { if (!meta[key]) { return undefined; @@ -30,6 +33,16 @@ export function useMenuTransform() { return Array.isArray(meta[key]) ? meta[key] : String(meta[key]).split(','); } function transformRoutes(menus: MenuDto[]): RouteRecordStringComponent[] { + const startupMenus = menus.filter((x) => x.startup); + if (startupMenus.length > 0) { + userStore.$patch((state) => { + state.userInfo && (state.userInfo.homePath = startupMenus[0]?.path); + }); + } else { + userStore.$patch((state) => { + state.userInfo && (state.userInfo.homePath = undefined); + }); + } const combMenus = menus.map((item) => { return { component: item.component.includes('BasicLayout') diff --git a/apps/vben5/packages/@abp/platform/src/index.ts b/apps/vben5/packages/@abp/platform/src/index.ts index 14fa9fe25..ca7a7275a 100644 --- a/apps/vben5/packages/@abp/platform/src/index.ts +++ b/apps/vben5/packages/@abp/platform/src/index.ts @@ -1,4 +1,5 @@ export * from './api'; export * from './components'; export * from './hooks'; +export * from './locales'; export * from './types'; diff --git a/apps/vben5/packages/@abp/platform/src/locales/index.ts b/apps/vben5/packages/@abp/platform/src/locales/index.ts new file mode 100644 index 000000000..ccf0b963b --- /dev/null +++ b/apps/vben5/packages/@abp/platform/src/locales/index.ts @@ -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; +} diff --git a/apps/vben5/packages/@abp/platform/src/locales/langs/en-US/workbench.json b/apps/vben5/packages/@abp/platform/src/locales/langs/en-US/workbench.json new file mode 100644 index 000000000..63034d52b --- /dev/null +++ b/apps/vben5/packages/@abp/platform/src/locales/langs/en-US/workbench.json @@ -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" + } + } +} diff --git a/apps/vben5/packages/@abp/platform/src/locales/langs/zh-CN/workbench.json b/apps/vben5/packages/@abp/platform/src/locales/langs/zh-CN/workbench.json new file mode 100644 index 000000000..c2561e71d --- /dev/null +++ b/apps/vben5/packages/@abp/platform/src/locales/langs/zh-CN/workbench.json @@ -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": "待办事项" + } + } +} diff --git a/apps/vben5/packages/@abp/platform/src/types/favorites.ts b/apps/vben5/packages/@abp/platform/src/types/favorites.ts new file mode 100644 index 000000000..221f13f48 --- /dev/null +++ b/apps/vben5/packages/@abp/platform/src/types/favorites.ts @@ -0,0 +1,34 @@ +import type { AuditedEntityDto, IHasConcurrencyStamp } from '@abp/core'; + +interface UserFavoriteMenuDto extends AuditedEntityDto { + 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, +}; diff --git a/apps/vben5/packages/@abp/platform/src/types/index.ts b/apps/vben5/packages/@abp/platform/src/types/index.ts index 73954c6db..3277e6293 100644 --- a/apps/vben5/packages/@abp/platform/src/types/index.ts +++ b/apps/vben5/packages/@abp/platform/src/types/index.ts @@ -1,4 +1,5 @@ export * from './dataDictionaries'; +export * from './favorites'; export * from './layouts'; export * from './menus'; export * from './messages'; diff --git a/apps/vben5/packages/@abp/saas/src/components/editions/EditionTable.vue b/apps/vben5/packages/@abp/saas/src/components/editions/EditionTable.vue index 601b3ff8f..e8ed79ccd 100644 --- a/apps/vben5/packages/@abp/saas/src/components/editions/EditionTable.vue +++ b/apps/vben5/packages/@abp/saas/src/components/editions/EditionTable.vue @@ -21,6 +21,7 @@ import { DeleteOutlined, EditOutlined, EllipsisOutlined, + PlusOutlined, } from '@ant-design/icons-vue'; import { Button, Dropdown, Menu, message, Modal } from 'ant-design-vue'; @@ -65,6 +66,7 @@ const gridOptions: VxeGridProps = { { align: 'left', field: 'displayName', + sortable: true, title: $t('AbpSaas.DisplayName:EditionName'), }, { @@ -83,8 +85,10 @@ const gridOptions: VxeGridProps = { keepSource: true, proxyConfig: { ajax: { - query: async ({ page }, formValues) => { + query: async ({ page, sort }, formValues) => { + const sorting = sort.order ? `${sort.field} ${sort.order}` : undefined; return await getPagedListApi({ + sorting, maxResultCount: page.pageSize, skipCount: (page.currentPage - 1) * page.pageSize, ...formValues, @@ -99,19 +103,23 @@ const gridOptions: VxeGridProps = { toolbarConfig: { custom: true, export: true, - // import: true, - refresh: true, + refresh: { + code: 'query', + }, zoom: true, }, }; const gridEvents: VxeGridListeners = { cellClick: () => {}, + sortChange: () => { + gridApi.query(); + }, }; const [EditionModal, modalApi] = useVbenModal({ connectedComponent: defineAsyncComponent(() => import('./EditionModal.vue')), }); -const [Grid, { query }] = useVbenVxeGrid({ +const [Grid, gridApi] = useVbenVxeGrid({ formOptions, gridEvents, gridOptions, @@ -145,7 +153,7 @@ const onDelete = (row: EditionDto) => { onOk: async () => { await deleteApi(row.id); message.success($t('AbpUi.DeletedSuccessfully')); - query(); + await gridApi.query(); }, title: $t('AbpUi.AreYouSure'), }); @@ -181,6 +189,7 @@ const onMenuClick = (row: EditionDto, info: MenuInfo) => { type="primary" v-access:code="[EditionsPermissions.Create]" @click="onCreate" + :icon="h(PlusOutlined)" > {{ $t('AbpSaas.NewEdition') }} @@ -233,7 +242,7 @@ const onMenuClick = (row: EditionDto, info: MenuInfo) => { - query()" /> + gridApi.query()" /> diff --git a/apps/vben5/packages/@abp/saas/src/components/tenants/TenantTable.vue b/apps/vben5/packages/@abp/saas/src/components/tenants/TenantTable.vue index 46d8cd856..e7973cee5 100644 --- a/apps/vben5/packages/@abp/saas/src/components/tenants/TenantTable.vue +++ b/apps/vben5/packages/@abp/saas/src/components/tenants/TenantTable.vue @@ -21,6 +21,7 @@ import { DeleteOutlined, EditOutlined, EllipsisOutlined, + PlusOutlined, } from '@ant-design/icons-vue'; import { Button, Dropdown, Menu, message, Modal } from 'ant-design-vue'; @@ -77,12 +78,14 @@ const gridOptions: VxeGridProps = { align: 'center', field: 'isActive', slots: { default: 'isActive' }, + sortable: true, title: $t('AbpSaas.DisplayName:IsActive'), width: 120, }, { align: 'left', field: 'name', + sortable: true, title: $t('AbpSaas.DisplayName:Name'), }, { @@ -107,8 +110,10 @@ const gridOptions: VxeGridProps = { keepSource: true, proxyConfig: { ajax: { - query: async ({ page }, formValues) => { + query: async ({ page, sort }, formValues) => { + const sorting = sort.order ? `${sort.field} ${sort.order}` : undefined; return await getPagedListApi({ + sorting, maxResultCount: page.pageSize, skipCount: (page.currentPage - 1) * page.pageSize, ...formValues, @@ -123,14 +128,18 @@ const gridOptions: VxeGridProps = { toolbarConfig: { custom: true, export: true, - // import: true, - refresh: true, + refresh: { + code: 'query', + }, zoom: true, }, }; const gridEvents: VxeGridListeners = { cellClick: () => {}, + sortChange: () => { + gridApi.query(); + }, }; const [TenantModal, modalApi] = useVbenModal({ connectedComponent: defineAsyncComponent(() => import('./TenantModal.vue')), @@ -146,7 +155,7 @@ const [TenantChangeDrawer, entityChangeDrawerApi] = useVbenDrawer({ const [TenantFeatureModal, tenantFeatureModalApi] = useVbenModal({ connectedComponent: FeatureModal, }); -const [Grid, { query }] = useVbenVxeGrid({ +const [Grid, gridApi] = useVbenVxeGrid({ formOptions, gridEvents, gridOptions, @@ -172,7 +181,7 @@ const onDelete = (row: TenantDto) => { onOk: async () => { await deleteApi(row.id); message.success($t('AbpUi.DeletedSuccessfully')); - query(); + await gridApi.query(); }, title: $t('AbpUi.AreYouSure'), }); @@ -213,6 +222,7 @@ const onMenuClick = (row: TenantDto, info: MenuInfo) => { type="primary" v-access:code="[TenantsPermissions.Create]" @click="onCreate" + :icon="h(PlusOutlined)" > {{ $t('AbpSaas.NewTenant') }} @@ -280,7 +290,10 @@ const onMenuClick = (row: TenantDto, info: MenuInfo) => { - query()" /> + gridApi.query()" + /> diff --git a/apps/vben5/packages/@abp/settings/src/components/definitions/SettingDefinitionTable.vue b/apps/vben5/packages/@abp/settings/src/components/definitions/SettingDefinitionTable.vue index c101d788b..86f8ace2b 100644 --- a/apps/vben5/packages/@abp/settings/src/components/definitions/SettingDefinitionTable.vue +++ b/apps/vben5/packages/@abp/settings/src/components/definitions/SettingDefinitionTable.vue @@ -5,12 +5,12 @@ import type { VbenFormProps } from '@vben/common-ui'; import type { SettingDefinitionDto } from '../../types/definitions'; -import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue'; +import { defineAsyncComponent, h, onMounted, ref } from 'vue'; import { useVbenModal } from '@vben/common-ui'; import { $t } from '@vben/locales'; -import { useLocalization, useLocalizationSerializer } from '@abp/core'; +import { sortby, useLocalization, useLocalizationSerializer } from '@abp/core'; import { useVbenVxeGrid } from '@abp/ui'; import { DeleteOutlined, @@ -26,12 +26,7 @@ defineOptions({ name: 'SettingDefinitionTable', }); -const permissionGroups = ref([]); -const pageState = reactive({ - current: 1, - size: 10, - total: 0, -}); +const settingGroups = ref([]); const { Lr } = useLocalization(); const { deserialize } = useLocalizationSerializer(); @@ -42,7 +37,6 @@ const formOptions: VbenFormProps = { collapsed: false, handleReset: onReset, async handleSubmit(params) { - pageState.current = 1; await onGet(params); }, schema: [ @@ -65,12 +59,14 @@ const gridOptions: VxeGridProps = { align: 'left', field: 'name', minWidth: 150, + sortable: true, title: $t('AbpSettingManagement.DisplayName:Name'), }, { align: 'left', field: 'displayName', minWidth: 150, + sortable: true, title: $t('AbpSettingManagement.DisplayName:DisplayName'), }, { @@ -83,6 +79,30 @@ const gridOptions: VxeGridProps = { ], exportConfig: {}, keepSource: true, + proxyConfig: { + ajax: { + query: async ({ page, sort }) => { + let items = sortby(settingGroups.value, sort.field); + if (sort.order === 'desc') { + items = items.reverse(); + } + const result = { + totalCount: settingGroups.value.length, + items: items.slice( + (page.currentPage - 1) * page.pageSize, + page.currentPage * page.pageSize, + ), + }; + return new Promise((resolve) => { + resolve(result); + }); + }, + }, + response: { + total: 'totalCount', + list: 'items', + }, + }, toolbarConfig: { custom: true, export: true, @@ -92,10 +112,8 @@ const gridOptions: VxeGridProps = { }; const gridEvents: VxeGridListeners = { - pageChange(params) { - pageState.current = params.currentPage; - pageState.size = params.pageSize; - onPageChange(); + sortChange: () => { + gridApi.query(); }, }; @@ -115,15 +133,14 @@ async function onGet(input?: Record) { try { gridApi.setLoading(true); const { items } = await getListApi(input); - pageState.total = items.length; - permissionGroups.value = items.map((item) => { + settingGroups.value = items.map((item) => { const localizableString = deserialize(item.displayName); return { ...item, displayName: Lr(localizableString.resourceName, localizableString.name), }; }); - onPageChange(); + setTimeout(() => gridApi.reload(), 100); } finally { gridApi.setLoading(false); } @@ -135,21 +152,6 @@ async function onReset() { await onGet(input); } -function onPageChange() { - const items = permissionGroups.value.slice( - (pageState.current - 1) * pageState.size, - pageState.current * pageState.size, - ); - gridApi.setGridOptions({ - data: items, - pagerConfig: { - currentPage: pageState.current, - pageSize: pageState.size, - total: pageState.total, - }, - }); -} - function onCreate() { modalApi.setData({}); modalApi.open(); diff --git a/apps/vben5/packages/@abp/settings/src/components/index.ts b/apps/vben5/packages/@abp/settings/src/components/index.ts index aaa211b75..53dbe96f7 100644 --- a/apps/vben5/packages/@abp/settings/src/components/index.ts +++ b/apps/vben5/packages/@abp/settings/src/components/index.ts @@ -1,3 +1,4 @@ export { default as SettingDefinitionTable } from './definitions/SettingDefinitionTable.vue'; +export { default as SettingForm } from './settings/SettingForm.vue'; export { default as SystemSetting } from './settings/SystemSetting.vue'; export { default as UserSetting } from './settings/UserSetting.vue'; diff --git a/apps/vben5/packages/@abp/settings/src/hooks/useDefineSettings.ts b/apps/vben5/packages/@abp/settings/src/hooks/useDefineSettings.ts index 628397795..502323bb3 100644 --- a/apps/vben5/packages/@abp/settings/src/hooks/useDefineSettings.ts +++ b/apps/vben5/packages/@abp/settings/src/hooks/useDefineSettings.ts @@ -67,7 +67,7 @@ export function useDefineSettings(): UserDefineSettingProvider { initlize, isTrue(name: string) { const value = getOrDefault(name, 'false'); - return value.toLowerCase() === 'true'; + return value?.toLowerCase() === 'true'; }, }; } diff --git a/apps/vben5/packages/@abp/tasks/src/components/job-infos/JobInfoTable.vue b/apps/vben5/packages/@abp/tasks/src/components/job-infos/JobInfoTable.vue index 23427239d..f656debe9 100644 --- a/apps/vben5/packages/@abp/tasks/src/components/job-infos/JobInfoTable.vue +++ b/apps/vben5/packages/@abp/tasks/src/components/job-infos/JobInfoTable.vue @@ -1,5 +1,4 @@ + + + + + + + + diff --git a/apps/vben5/packages/@abp/wechat/src/components/index.ts b/apps/vben5/packages/@abp/wechat/src/components/index.ts new file mode 100644 index 000000000..8e84134b5 --- /dev/null +++ b/apps/vben5/packages/@abp/wechat/src/components/index.ts @@ -0,0 +1,2 @@ +export { default as WechatWorkUserBinder } from './bind-user/index.vue'; +export { default as WechatSettings } from './settings/index.vue'; diff --git a/apps/vben5/packages/@abp/wechat/src/components/settings/index.vue b/apps/vben5/packages/@abp/wechat/src/components/settings/index.vue new file mode 100644 index 000000000..e1f3e30a6 --- /dev/null +++ b/apps/vben5/packages/@abp/wechat/src/components/settings/index.vue @@ -0,0 +1,37 @@ + + + + + + + diff --git a/apps/vben5/packages/@abp/wechat/src/index.ts b/apps/vben5/packages/@abp/wechat/src/index.ts new file mode 100644 index 000000000..0ef464305 --- /dev/null +++ b/apps/vben5/packages/@abp/wechat/src/index.ts @@ -0,0 +1,2 @@ +export * from './api'; +export * from './components'; diff --git a/apps/vben5/packages/@abp/wechat/src/types/js-sdk.ts b/apps/vben5/packages/@abp/wechat/src/types/js-sdk.ts new file mode 100644 index 000000000..44031b6b5 --- /dev/null +++ b/apps/vben5/packages/@abp/wechat/src/types/js-sdk.ts @@ -0,0 +1,6 @@ +interface AgentConfigDto { + agentId: string; + corpId: string; +} + +export type { AgentConfigDto }; diff --git a/apps/vben5/packages/@abp/wechat/tsconfig.json b/apps/vben5/packages/@abp/wechat/tsconfig.json new file mode 100644 index 000000000..ce1a891fb --- /dev/null +++ b/apps/vben5/packages/@abp/wechat/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue b/apps/vben5/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue index f3eb6735a..9844c409d 100644 --- a/apps/vben5/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue +++ b/apps/vben5/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue @@ -300,7 +300,7 @@ async function init() { const enableProxyConfig = options.value.proxyConfig?.enabled; if (enableProxyConfig && autoLoad) { props.api.grid.commitProxy?.( - '_init', + 'initial', formOptions.value ? ((await formApi.getValues()) ?? {}) : {}, ); // props.api.reload(formApi.form?.values ?? {}); diff --git a/apps/vben5/pnpm-workspace.yaml b/apps/vben5/pnpm-workspace.yaml index 4acb3954f..42210ddfc 100644 --- a/apps/vben5/pnpm-workspace.yaml +++ b/apps/vben5/pnpm-workspace.yaml @@ -51,10 +51,12 @@ catalog: '@types/lodash.debounce': ^4.0.9 '@types/lodash.get': ^4.4.9 '@types/lodash.groupby': ^4.6.9 + '@types/lodash.isdate': ^4.0.9 '@types/lodash.isequal': ^4.5.8 '@types/lodash.isnumber': ^3.0.9 '@types/lodash.merge': ^4.6.9 '@types/lodash.set': ^4.3.9 + '@types/lodash.sortby': ^4.7.9 '@types/node': ^22.15.3 '@types/nprogress': ^0.2.3 '@types/postcss-import': ^14.0.3 @@ -73,6 +75,7 @@ catalog: '@vueuse/core': ^13.1.0 '@vueuse/integrations': ^13.1.0 '@vueuse/motion': ^3.0.3 + '@wecom/jssdk': ^2.3.1 ant-design-vue: ^4.2.6 archiver: ^7.0.1 autoprefixer: ^10.4.21 @@ -130,10 +133,12 @@ catalog: lodash.debounce: ^4.0.8 lodash.get: ^4.4.2 lodash.groupby: ^4.6.0 + lodash.isdate: ^4.0.1 lodash.isequal: ^4.5.0 lodash.isnumber: ^3.0.3 lodash.merge: ^4.6.2 lodash.set: ^4.3.2 + lodash.sortby: ^4.7.0 lucide-vue-next: ^0.507.0 medium-zoom: ^1.1.0 naive-ui: ^2.41.0 @@ -205,8 +210,9 @@ catalog: vue-simple-uploader: ^1.0.3 vue-tippy: ^6.7.0 vue-tsc: 2.2.10 - vxe-pc-ui: ^4.5.35 - vxe-table: ^4.13.16 + vue3-colorpicker: ^2.3.0 + vxe-pc-ui: ^4.7.12 + vxe-table: ^4.14.4 watermark-js-plus: ^1.6.0 zod: ^3.24.3 zod-defaults: ^0.1.3 diff --git a/apps/vue/src/router/routes/basic.ts b/apps/vue/src/router/routes/basic.ts index a8fcb220e..c18196a75 100644 --- a/apps/vue/src/router/routes/basic.ts +++ b/apps/vue/src/router/routes/basic.ts @@ -10,7 +10,7 @@ import { // 404 on a page export const PAGE_NOT_FOUND_ROUTE: AppRouteRecordRaw = { path: '/:path(.*)*', - name: PAGE_NOT_FOUND_NAME, + name: 'ErrorPage', component: LAYOUT, meta: { title: 'ErrorPage', diff --git a/aspnet-core/cleanup-logs.bat b/aspnet-core/cleanup-logs.bat index ef5471574..5ffd61025 100644 --- a/aspnet-core/cleanup-logs.bat +++ b/aspnet-core/cleanup-logs.bat @@ -4,12 +4,17 @@ chcp 65001 echo. 清理所有服务日志 +del .\services\LY.MicroService.Applications.Single\Logs /Q del .\services\LY.MicroService.BackendAdmin.HttpApi.Host\Logs /Q +del .\services\LY.MicroService.AuthServer\Logs /Q +del .\services\LY.MicroService.AuthServer.HttpApi.Host\Logs /Q del .\services\LY.MicroService.identityServer\Logs /Q del .\services\LY.MicroService.identityServer.HttpApi.Host\Logs /Q del .\services\LY.MicroService.LocalizationManagement.HttpApi.Host\Logs /Q del .\services\LY.MicroService.PlatformManagement.HttpApi.Host\Logs /Q del .\services\LY.MicroService.RealtimeMessage.HttpApi.Host\Logs /Q del .\services\LY.MicroService.TaskManagement.HttpApi.Host\Logs /Q +del .\services\LY.MicroService.WebhooksManagement.HttpApi.Host\Logs /Q +del .\services\LY.MicroService.WechatManagement.HttpApi.Host\Logs /Q del .\services\LY.MicroService.WorkflowManagement.HttpApi.Host\Logs /Q diff --git a/aspnet-core/framework/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/TencentCloudSmsSender.cs b/aspnet-core/framework/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/TencentCloudSmsSender.cs index 501283863..0ffa5d62d 100644 --- a/aspnet-core/framework/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/TencentCloudSmsSender.cs +++ b/aspnet-core/framework/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/TencentCloudSmsSender.cs @@ -48,6 +48,9 @@ public class TencentCloudSmsSender : ISmsSender, ITransientDependency Check.NotNullOrWhiteSpace(appId, TencentCloudSettingNames.Sms.AppId); + // 短信模板相关参数 + List templateParams = ["TemplateCode", "SignName"]; + // 统一使用 TemplateCode作为模板参数, 解决不一样的sms提供商参数差异 if (!smsMessage.Properties.TryGetValue("TemplateCode", out var templateId)) { @@ -69,7 +72,8 @@ public class TencentCloudSmsSender : ISmsSender, ITransientDependency if (smsMessage.Properties.Any()) { - request.TemplateParamSet = smsMessage.Properties.Select(x => x.Value.ToString()).ToArray(); + // 去掉短信模板相关参数,只保留要用的变量 + request.TemplateParamSet = smsMessage.Properties.Where(x => !templateParams.Contains(x.Key)).Select(x => x.Value.ToString()).ToArray(); } var smsClient = await TencentCloudClientFactory.CreateAsync(); diff --git a/aspnet-core/framework/common/LINGYUN.Abp.IP2Region/LINGYUN/Abp/IP2Region/AbpIP2RegionModule.cs b/aspnet-core/framework/common/LINGYUN.Abp.IP2Region/LINGYUN/Abp/IP2Region/AbpIP2RegionModule.cs index ab95964e9..863470a46 100644 --- a/aspnet-core/framework/common/LINGYUN.Abp.IP2Region/LINGYUN/Abp/IP2Region/AbpIP2RegionModule.cs +++ b/aspnet-core/framework/common/LINGYUN.Abp.IP2Region/LINGYUN/Abp/IP2Region/AbpIP2RegionModule.cs @@ -29,7 +29,7 @@ public class AbpIP2RegionModule : AbpModule Configure(options => { - options.IPLocationResolvers.Add(new IP2RegionIPLocationResolveContributorBase()); + options.IPLocationResolvers.Add(new IP2RegionIPLocationResolveContributor()); }); } } diff --git a/aspnet-core/framework/common/LINGYUN.Abp.IP2Region/LINGYUN/Abp/IP2Region/IP2RegionIPLocationResolveContributorBase.cs b/aspnet-core/framework/common/LINGYUN.Abp.IP2Region/LINGYUN/Abp/IP2Region/IP2RegionIPLocationResolveContributor.cs similarity index 96% rename from aspnet-core/framework/common/LINGYUN.Abp.IP2Region/LINGYUN/Abp/IP2Region/IP2RegionIPLocationResolveContributorBase.cs rename to aspnet-core/framework/common/LINGYUN.Abp.IP2Region/LINGYUN/Abp/IP2Region/IP2RegionIPLocationResolveContributor.cs index 0cc4c2b44..4abb90200 100644 --- a/aspnet-core/framework/common/LINGYUN.Abp.IP2Region/LINGYUN/Abp/IP2Region/IP2RegionIPLocationResolveContributorBase.cs +++ b/aspnet-core/framework/common/LINGYUN.Abp.IP2Region/LINGYUN/Abp/IP2Region/IP2RegionIPLocationResolveContributor.cs @@ -5,7 +5,7 @@ using System; using System.Threading.Tasks; namespace LINGYUN.Abp.IP2Region; -public class IP2RegionIPLocationResolveContributorBase : IPLocationResolveContributorBase +public class IP2RegionIPLocationResolveContributor : IPLocationResolveContributorBase { public const string ContributorName = "IP2Region"; public override string Name => ContributorName; diff --git a/aspnet-core/framework/common/LINGYUN.Abp.RealTime/LINGYUN/Abp/RealTime/Localization/LocalizableStringInfo.cs b/aspnet-core/framework/common/LINGYUN.Abp.RealTime/LINGYUN/Abp/RealTime/Localization/LocalizableStringInfo.cs index 10e29e245..ecdfc31c0 100644 --- a/aspnet-core/framework/common/LINGYUN.Abp.RealTime/LINGYUN/Abp/RealTime/Localization/LocalizableStringInfo.cs +++ b/aspnet-core/framework/common/LINGYUN.Abp.RealTime/LINGYUN/Abp/RealTime/Localization/LocalizableStringInfo.cs @@ -24,6 +24,7 @@ public class LocalizableStringInfo /// public LocalizableStringInfo() { + Values = new Dictionary(); } /// /// Instantiate @@ -38,6 +39,6 @@ public class LocalizableStringInfo { ResourceName = resourceName; Name = name; - Values = values; + Values = values ?? new Dictionary(); } } diff --git a/aspnet-core/framework/open-api/LINGYUN.Abp.OpenApi/LINGYUN/Abp/OpenApi/AppDescriptor.cs b/aspnet-core/framework/open-api/LINGYUN.Abp.OpenApi/LINGYUN/Abp/OpenApi/AppDescriptor.cs index 74a2d6889..149d3e5c1 100644 --- a/aspnet-core/framework/open-api/LINGYUN.Abp.OpenApi/LINGYUN/Abp/OpenApi/AppDescriptor.cs +++ b/aspnet-core/framework/open-api/LINGYUN.Abp.OpenApi/LINGYUN/Abp/OpenApi/AppDescriptor.cs @@ -17,7 +17,7 @@ public class AppDescriptor /// /// 应用token /// - public string AppToken { get; set; } + public string? AppToken { get; set; } /// /// 签名有效时间 /// 单位: s @@ -29,7 +29,7 @@ public class AppDescriptor string appName, string appKey, string appSecret, - string appToken = null, + string? appToken = null, int? signLifeTime = null) { AppName = appName; diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkInternalUserFinder.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkUserClaimProvider.cs similarity index 66% rename from aspnet-core/framework/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkInternalUserFinder.cs rename to aspnet-core/framework/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkUserClaimProvider.cs index eb44979a6..fda85cb68 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkInternalUserFinder.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkUserClaimProvider.cs @@ -8,16 +8,17 @@ using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; +using Volo.Abp.Uow; namespace LINGYUN.Abp.Identity.WeChat.Work; [Dependency(ServiceLifetime.Transient, ReplaceServices = true)] -[ExposeServices(typeof(IWeChatWorkInternalUserFinder))] -public class WeChatWorkInternalUserFinder : IWeChatWorkInternalUserFinder +[ExposeServices(typeof(IWeChatWorkUserClaimProvider))] +public class WeChatWorkUserClaimProvider : IWeChatWorkUserClaimProvider { protected IdentityUserManager UserManager { get; } - public WeChatWorkInternalUserFinder( + public WeChatWorkUserClaimProvider( IdentityUserManager userManager) { UserManager = userManager; @@ -55,4 +56,22 @@ public class WeChatWorkInternalUserFinder : IWeChatWorkInternalUserFinder return userIdentifiers; } + + [UnitOfWork] + public async virtual Task BindUserAsync( + Guid userId, + string weChatUserId, + CancellationToken cancellationToken = default) + { + var user = await UserManager.GetByIdAsync(userId); + var existsWeChatUserId = GetUserOpenIdOrNull(user, AbpWeChatWorkGlobalConsts.ProviderName); + if (!existsWeChatUserId.IsNullOrWhiteSpace()) + { + user.RemoveLogin(AbpWeChatWorkGlobalConsts.ProviderName, existsWeChatUserId); + } + user.AddLogin(new Microsoft.AspNetCore.Identity.UserLoginInfo( + AbpWeChatWorkGlobalConsts.ProviderName, + weChatUserId, + AbpWeChatWorkGlobalConsts.DisplayName)); + } } diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeAppService.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeAppService.cs index 06f8a7b06..d7369ae4b 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeAppService.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeAppService.cs @@ -4,12 +4,24 @@ using Volo.Abp.Application.Services; namespace LINGYUN.Abp.WeChat.Work.Authorize; public interface IWeChatWorkAuthorizeAppService : IApplicationService { + /// + /// 生成授权链接 + /// + /// 授权回调Url名称 + /// 响应类型 + /// 授权范围 + /// Task GenerateOAuth2AuthorizeAsync( - string redirectUri, + string urlName, string responseType = "code", string scope = "snsapi_base"); - + /// + /// 生成登录链接 + /// + /// 授权回调Url名称 + /// 登录类型 + /// Task GenerateOAuth2LoginAsync( - string redirectUri, - string loginType = "ServiceApp"); + string urlName, + string loginType = "CorpApp"); } diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/Dtos/AgentConfigDto.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/Dtos/AgentConfigDto.cs new file mode 100644 index 000000000..4ce84bffb --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/Dtos/AgentConfigDto.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Abp.WeChat.Work.JsSdk.Dtos; +public class AgentConfigDto +{ + public string AgentId { get; set; } + public string CorpId { get; set; } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/Dtos/JsApiSignatureDto.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/Dtos/JsApiSignatureDto.cs new file mode 100644 index 000000000..67d321223 --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/Dtos/JsApiSignatureDto.cs @@ -0,0 +1,17 @@ +namespace LINGYUN.Abp.WeChat.Work.JsSdk.Dtos; +public class JsApiSignatureDto +{ + public string Nonce { get; set; } + public string Timestamp { get; set; } + public string Signature { get; set; } + public JsApiSignatureDto() + { + + } + public JsApiSignatureDto(string nonce, string timestamp, string signature) + { + Nonce = nonce; + Timestamp = timestamp; + Signature = signature; + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/IWeChatWorkJsSdkAppService.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/IWeChatWorkJsSdkAppService.cs new file mode 100644 index 000000000..daccd6f7f --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/IWeChatWorkJsSdkAppService.cs @@ -0,0 +1,13 @@ +using LINGYUN.Abp.WeChat.Work.JsSdk.Dtos; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace LINGYUN.Abp.WeChat.Work.JsSdk; +public interface IWeChatWorkJsSdkAppService : IApplicationService +{ + Task GetAgentConfigAsync(); + + Task GetSignatureAsync(string url); + + Task GetAgentSignatureAsync(string url); +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN.Abp.WeChat.Work.Application.csproj b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN.Abp.WeChat.Work.Application.csproj index 283c07e8f..8ce77df14 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN.Abp.WeChat.Work.Application.csproj +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN.Abp.WeChat.Work.Application.csproj @@ -15,6 +15,7 @@ + diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkApplicationModule.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkApplicationModule.cs index 0eb380c9c..be9618245 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkApplicationModule.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkApplicationModule.cs @@ -1,11 +1,13 @@ using Volo.Abp.Application; using Volo.Abp.Modularity; +using Volo.Abp.UI.Navigation; namespace LINGYUN.Abp.WeChat.Work; [DependsOn( typeof(AbpWeChatWorkApplicationContractsModule), typeof(AbpWeChatWorkModule), + typeof(AbpUiNavigationModule), typeof(AbpDddApplicationModule))] public class AbpWeChatWorkApplicationModule : AbpModule { diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeAppService.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeAppService.cs index cf3112900..13ab2f28a 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeAppService.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeAppService.cs @@ -1,42 +1,49 @@ -using LINGYUN.Abp.WeChat.Work.Settings; -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.Application.Services; using Volo.Abp.Security.Encryption; +using Volo.Abp.UI.Navigation.Urls; +using Volo.Abp.Users; namespace LINGYUN.Abp.WeChat.Work.Authorize; [IntegrationService] public class WeChatWorkAuthorizeAppService : ApplicationService, IWeChatWorkAuthorizeAppService { + private readonly IAppUrlProvider _appUrlProvider; private readonly IStringEncryptionService _encryptionService; private readonly IWeChatWorkAuthorizeGenerator _authorizeGenerator; public WeChatWorkAuthorizeAppService( + IAppUrlProvider appUrlProvider, IStringEncryptionService encryptionService, IWeChatWorkAuthorizeGenerator authorizeGenerator) { + _appUrlProvider = appUrlProvider; _encryptionService = encryptionService; _authorizeGenerator = authorizeGenerator; } - public async virtual Task GenerateOAuth2AuthorizeAsync(string redirectUri, string responseType = "code", string scope = "snsapi_base") + public async virtual Task GenerateOAuth2AuthorizeAsync( + string urlName, + string responseType = "code", + string scope = "snsapi_base") { - - var state = _encryptionService.Encrypt($"redirectUri={redirectUri}&responseType={responseType}&scope={scope}&random={Guid.NewGuid():D}").ToMd5(); + var userId = CurrentUser.GetId().ToString("D"); + var state = _encryptionService.Encrypt(userId); + var redirectUri = await _appUrlProvider.GetUrlAsync(AbpWeChatWorkGlobalConsts.ProviderName, urlName); return await _authorizeGenerator.GenerateOAuth2AuthorizeAsync(redirectUri, state, responseType, scope); } - public async virtual Task GenerateOAuth2LoginAsync(string redirectUri, string loginType = "ServiceApp") + public async virtual Task GenerateOAuth2LoginAsync( + string urlName, + string loginType = "CorpApp") { - var state = _encryptionService.Encrypt($"redirectUri={redirectUri}&loginType={loginType}&random={Guid.NewGuid():D}").ToMd5(); - - var corpId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.CorpId); - - Check.NotNullOrEmpty(corpId, nameof(corpId)); + var userId = CurrentUser.GetId().ToString("D"); + var state = _encryptionService.Encrypt(userId); + var redirectUri = await _appUrlProvider.GetUrlAsync(AbpWeChatWorkGlobalConsts.ProviderName, urlName); - return await _authorizeGenerator.GenerateOAuth2LoginAsync(corpId, redirectUri, state, loginType); + return await _authorizeGenerator.GenerateOAuth2LoginAsync(redirectUri, state, loginType); } } diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/JsSdk/WeChatWorkJsSdkAppService.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/JsSdk/WeChatWorkJsSdkAppService.cs new file mode 100644 index 000000000..803c17395 --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/JsSdk/WeChatWorkJsSdkAppService.cs @@ -0,0 +1,50 @@ +using LINGYUN.Abp.WeChat.Work.Features; +using LINGYUN.Abp.WeChat.Work.JsSdk.Dtos; +using LINGYUN.Abp.WeChat.Work.Settings; +using Microsoft.AspNetCore.Authorization; +using System.Threading.Tasks; +using System.Web; +using Volo.Abp.Application.Services; +using Volo.Abp.Features; + +namespace LINGYUN.Abp.WeChat.Work.JsSdk; + +[Authorize] +[RequiresFeature(WeChatWorkFeatureNames.Enable)] +public class WeChatWorkJsSdkAppService : ApplicationService, IWeChatWorkJsSdkAppService +{ + private readonly IJsApiTicketProvider _ticketProvider; + + public WeChatWorkJsSdkAppService(IJsApiTicketProvider ticketProvider) + { + _ticketProvider = ticketProvider; + } + + public async virtual Task GetAgentConfigAsync() + { + var corpId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.CorpId); + var agentId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.AgentId); + + return new AgentConfigDto + { + CorpId = corpId, + AgentId = agentId, + }; + } + + public async virtual Task GetAgentSignatureAsync(string url) + { + var jsApiTicket = await _ticketProvider.GetAgentTicketInfoAsync(); + var signatureData = _ticketProvider.GenerateSignature(jsApiTicket, HttpUtility.UrlDecode(url)); + + return new JsApiSignatureDto(signatureData.Nonce, signatureData.Timestamp, signatureData.Signature); + } + + public async virtual Task GetSignatureAsync(string url) + { + var jsApiTicket = await _ticketProvider.GetTicketInfoAsync(); + var signatureData = _ticketProvider.GenerateSignature(jsApiTicket, HttpUtility.UrlDecode(url)); + + return new JsApiSignatureDto(signatureData.Nonce, signatureData.Timestamp, signatureData.Signature); + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeController.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeController.cs index c32f652fe..9130cf8d5 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeController.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.AspNetCore.Mvc; @@ -34,19 +35,19 @@ public class WeChatWorkAuthorizeController : AbpControllerBase, IWeChatWorkAutho /// /// 详情见企业微信文档: /// - /// 企业内部应用标识 - /// 登录成功重定向url + /// 授权回调Url名称 /// oauth响应类型 /// oauth授权范围 /// [HttpGet] - [Route("oauth2")] + [Authorize] + [Route("oauth2/generate")] public virtual Task GenerateOAuth2AuthorizeAsync( - [FromQuery(Name = "redirect_uri")] string redirectUri, + [FromQuery] string urlName, [FromQuery(Name = "response_type")] string responseType = "code", [FromQuery] string scope = "snsapi_base") { - return _service.GenerateOAuth2AuthorizeAsync(redirectUri, responseType, scope); + return _service.GenerateOAuth2AuthorizeAsync(responseType, scope); } /// @@ -55,19 +56,19 @@ public class WeChatWorkAuthorizeController : AbpControllerBase, IWeChatWorkAutho /// /// 详情见企业微信文档: /// - /// 企业内部应用标识 - /// 登录成功重定向url + /// 授权回调Url名称 /// oauth响应类型 /// oauth授权范围 /// [HttpGet] - [Route("oauth2/authorize")] + [Authorize] + [Route("oauth2")] public async virtual Task OAuth2AuthorizeAsync( - [FromQuery(Name = "redirect_uri")] string redirectUri, + [FromQuery] string urlName, [FromQuery(Name = "response_type")] string responseType = "code", [FromQuery] string scope = "snsapi_base") { - var url = await _service.GenerateOAuth2AuthorizeAsync(redirectUri, responseType, scope); + var url = await _service.GenerateOAuth2AuthorizeAsync(urlName, responseType, scope); return Redirect(url); } @@ -78,17 +79,17 @@ public class WeChatWorkAuthorizeController : AbpControllerBase, IWeChatWorkAutho /// /// 详情见企业微信文档: /// - /// 登录成功重定向url + /// 授权回调Url名称 /// 登录类型, ServiceApp:服务商登录;CorpApp:企业自建/代开发应用登录 - /// 企业自建应用/服务商代开发应用 AgentID,当login_type=CorpApp时填写 /// [HttpGet] - [Route("oauth2/login")] + [Authorize] + [Route("oauth2/login/generate")] public virtual Task GenerateOAuth2LoginAsync( - [FromQuery(Name = "redirect_uri")] string redirectUri, - [FromQuery(Name = "login_type")] string loginType = "ServiceApp") + string urlName, + string loginType = "CorpApp") { - return _service.GenerateOAuth2LoginAsync(redirectUri, loginType); + return _service.GenerateOAuth2LoginAsync(urlName, loginType); } /// @@ -97,17 +98,17 @@ public class WeChatWorkAuthorizeController : AbpControllerBase, IWeChatWorkAutho /// /// 详情见企业微信文档: /// - /// 登录成功重定向url + /// 授权回调Url名称 /// 登录类型, ServiceApp:服务商登录;CorpApp:企业自建/代开发应用登录 - /// 企业自建应用/服务商代开发应用 AgentID,当login_type=CorpApp时填写 /// [HttpGet] - [Route("oauth2/login/sso")] + [Authorize] + [Route("oauth2/login")] public async virtual Task OAuth2LoginAsync( - [FromQuery(Name = "redirect_uri")] string redirectUri, - [FromQuery(Name = "login_type")] string loginType = "ServiceApp") + string urlName, + string loginType = "CorpApp") { - var url = await _service.GenerateOAuth2LoginAsync(redirectUri, loginType); + var url = await _service.GenerateOAuth2LoginAsync(urlName, loginType); return Redirect(url); } diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/JsSdk/WeChatWorkJsSdkController.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/JsSdk/WeChatWorkJsSdkController.cs new file mode 100644 index 000000000..e7567ccfe --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/JsSdk/WeChatWorkJsSdkController.cs @@ -0,0 +1,46 @@ +using LINGYUN.Abp.WeChat.Work.JsSdk.Dtos; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Auditing; + +namespace LINGYUN.Abp.WeChat.Work.JsSdk; + +[Authorize] +[Controller] +[DisableAuditing] +[Route("api/wechat/work/jssdk")] +[Area(AbpWeChatWorkRemoteServiceConsts.ModuleName)] +[RemoteService(Name = AbpWeChatWorkRemoteServiceConsts.RemoteServiceName)] +public class WeChatWorkJsSdkController : AbpControllerBase, IWeChatWorkJsSdkAppService +{ + private readonly IWeChatWorkJsSdkAppService _service; + + public WeChatWorkJsSdkController(IWeChatWorkJsSdkAppService service) + { + _service = service; + } + + [HttpGet] + [Route("agent-config")] + public virtual Task GetAgentConfigAsync() + { + return _service.GetAgentConfigAsync(); + } + + [HttpGet] + [Route("agent-signature")] + public virtual Task GetAgentSignatureAsync(string url) + { + return _service.GetAgentSignatureAsync(url); + } + + [HttpGet] + [Route("signature")] + public virtual Task GetSignatureAsync(string url) + { + return _service.GetSignatureAsync(url); + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkGlobalConsts.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkGlobalConsts.cs index d2722e389..21f8856b5 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkGlobalConsts.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkGlobalConsts.cs @@ -5,7 +5,7 @@ public class AbpWeChatWorkGlobalConsts /// /// 企业微信对应的Provider名称 /// - public static string ProviderName { get; set; } = "WeCom"; + public static string ProviderName { get; set; } = "WorkWeixin"; /// /// 企业微信授权类型 /// diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeGenerator.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeGenerator.cs index 1b009c805..121915242 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeGenerator.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeGenerator.cs @@ -25,13 +25,13 @@ public interface IWeChatWorkAuthorizeGenerator /// /// /// - /// + /// /// /// Task GenerateOAuth2LoginAsync( string redirectUri, string state, string loginType = "ServiceApp", - string agentid = "", + string agentId = "", string lang = "zh"); } diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkInternalUserFinder.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkUserClaimProvider.cs similarity index 63% rename from aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkInternalUserFinder.cs rename to aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkUserClaimProvider.cs index 42a0511b7..9a6b19960 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkInternalUserFinder.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkUserClaimProvider.cs @@ -4,7 +4,10 @@ using System.Threading; using System.Threading.Tasks; namespace LINGYUN.Abp.WeChat.Work.Authorize; -public interface IWeChatWorkInternalUserFinder +/// +/// 企业微信用户身份提供者 +/// +public interface IWeChatWorkUserClaimProvider { /// /// 通过用户标识查询企业微信用户标识 @@ -24,4 +27,15 @@ public interface IWeChatWorkInternalUserFinder Task> FindUserIdentifierListAsync( IEnumerable userIdList, CancellationToken cancellationToken = default); + /// + /// 绑定用户企业微信 + /// + /// 用户Id + /// 企业微信用户Id + /// + /// + Task BindUserAsync( + Guid userId, + string weChatUserId, + CancellationToken cancellationToken = default); } diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkInternalUserFinder.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkUserClaimProvider.cs similarity index 61% rename from aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkInternalUserFinder.cs rename to aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkUserClaimProvider.cs index d9ac3748b..cc4921f84 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkInternalUserFinder.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkUserClaimProvider.cs @@ -8,9 +8,9 @@ using Volo.Abp.DependencyInjection; namespace LINGYUN.Abp.WeChat.Work.Authorize; [Dependency(ServiceLifetime.Singleton, TryRegister = true)] -public class NullWeChatWorkInternalUserFinder : IWeChatWorkInternalUserFinder +public class NullWeChatWorkUserClaimProvider : IWeChatWorkUserClaimProvider { - public readonly static IWeChatWorkInternalUserFinder Instance = new NullWeChatWorkInternalUserFinder(); + public readonly static IWeChatWorkUserClaimProvider Instance = new NullWeChatWorkUserClaimProvider(); public Task FindUserIdentifierAsync( Guid userId, CancellationToken cancellationToken = default) @@ -25,4 +25,11 @@ public class NullWeChatWorkInternalUserFinder : IWeChatWorkInternalUserFinder { return Task.FromResult(new List()); } + public Task BindUserAsync( + Guid userId, + string weChatUserId, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException("请使用 AbpIdentityWeChatWorkModule 模块实现企业微信用户绑定!"); + } } diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeGenerator.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeGenerator.cs index bf684ed89..319b3d2f4 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeGenerator.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeGenerator.cs @@ -57,14 +57,19 @@ public class WeChatWorkAuthorizeGenerator : IWeChatWorkAuthorizeGenerator, ISing } public async virtual Task GenerateOAuth2LoginAsync( - string appid, string redirectUri, string state, string loginType = "ServiceApp", + string agentId = "", string lang = "zh") { - var agentId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.AgentId); + if (agentId.IsNullOrWhiteSpace()) + { + agentId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.AgentId); + } + var corpId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.CorpId); + Check.NotNullOrEmpty(corpId, nameof(corpId)); Check.NotNullOrEmpty(agentId, nameof(agentId)); var client = HttpClientFactory.CreateClient(AbpWeChatWorkGlobalConsts.LoginClient); @@ -75,7 +80,7 @@ public class WeChatWorkAuthorizeGenerator : IWeChatWorkAuthorizeGenerator, ISing .Append(client.BaseAddress.AbsoluteUri.EnsureEndsWith('/')) .Append("wwlogin/sso/login") .AppendFormat("?login_type={0}", loginType) - .AppendFormat("&appid={0}", appid) + .AppendFormat("&appid={0}", corpId) .AppendFormat("&agentid={0}", agentId) .AppendFormat("&redirect_uri={0}", HttpUtility.UrlEncode(redirectUri)) .AppendFormat("&state={0}", state) diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/IJsApiTicketProvider.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/IJsApiTicketProvider.cs new file mode 100644 index 000000000..59dde2512 --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/IJsApiTicketProvider.cs @@ -0,0 +1,32 @@ +using LINGYUN.Abp.WeChat.Work.JsSdk.Models; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.WeChat.Work.JsSdk; +/// +/// JS-SDK临时票据提供者 +/// See: https://developer.work.weixin.qq.com/document/path/90506 +/// +public interface IJsApiTicketProvider +{ + /// + /// 获取企业 jsapi_ticket + /// + /// + /// + Task GetTicketInfoAsync(CancellationToken cancellationToken = default); + /// + /// 获取应用 jsapi_ticket + /// + /// + /// + Task GetAgentTicketInfoAsync(CancellationToken cancellationToken = default); + /// + /// 获取JS-SDK签名 + /// + /// JS-SDK临时票据 + /// 生成签名的url + /// + /// + JsApiSignatureData GenerateSignature(JsApiTicketInfo ticketInfo, string url, CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/JsApiTicketHelper.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/JsApiTicketHelper.cs new file mode 100644 index 000000000..f600da4fc --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/JsApiTicketHelper.cs @@ -0,0 +1,65 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace LINGYUN.Abp.WeChat.Work.JsSdk; +public static class JsApiTicketHelper +{ + private static string[] _randomChars = new string[] + { + "a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z", + "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z" + }; + + public static string GenerateNonce() + { + var r = new Random(); + var sb = new StringBuilder(); + var length = _randomChars.Length; + for (var i = 0; i < 15; i++) + { + sb.Append(_randomChars[r.Next(length - 1)]); + } + return sb.ToString(); + } + + public static long GenerateTimestamp() + { + return (DateTime.Now.ToUniversalTime().Ticks - 621355968000000000) / 10000000; + } + + private static string ToSha1(string str) + { + using (var sha = SHA1.Create()) + { + var data = sha.ComputeHash(Encoding.UTF8.GetBytes(str)); + + var sb = new StringBuilder(); + foreach (var d in data) + { + sb.Append(d.ToString("x2")); + } + return sb.ToString(); + } + } + /// + /// 生成JS-SDK签名 + /// See: https://developer.work.weixin.qq.com/document/path/90506 + /// + /// + /// + /// + public static string GenerateSignature( + string jsapiTicket, + string nonce, + string timestamp, + string url) + { + var sb = new StringBuilder(); + sb.Append("jsapi_ticket=").Append(jsapiTicket).Append("&") + .Append("noncestr=").Append(nonce).Append("&") + .Append("timestamp=").Append(timestamp).Append("&") + .Append("url=").Append(url.IndexOf("#") >= 0 ? url.Substring(0, url.IndexOf("#")) : url); + return ToSha1(sb.ToString()); + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/JsApiTicketProvider.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/JsApiTicketProvider.cs new file mode 100644 index 000000000..934e125e7 --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/JsApiTicketProvider.cs @@ -0,0 +1,105 @@ +using LINGYUN.Abp.WeChat.Work.JsSdk.Models; +using LINGYUN.Abp.WeChat.Work.Token; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.WeChat.Work.JsSdk; +public class JsApiTicketProvider : IJsApiTicketProvider, ISingletonDependency +{ + public ILogger Logger { get; set; } + + protected IHttpClientFactory HttpClientFactory { get; } + protected IDistributedCache Cache { get; } + protected IWeChatWorkTokenProvider WeChatWorkTokenProvider { get; } + + public JsApiTicketProvider( + IHttpClientFactory httpClientFactory, + IWeChatWorkTokenProvider weChatWorkTokenProvider, + IDistributedCache cache) + { + WeChatWorkTokenProvider = weChatWorkTokenProvider; + HttpClientFactory = httpClientFactory; + Cache = cache; + + Logger = NullLogger.Instance; + } + + public async virtual Task GetAgentTicketInfoAsync(CancellationToken cancellationToken = default) + { + var cacheKey = nameof(GetAgentTicketInfoAsync); + var token = await WeChatWorkTokenProvider.GetTokenAsync(cancellationToken); + var cackeItem = await GetCacheItemAsync( + cacheKey, + $"/cgi-bin/ticket/get?access_token={token.AccessToken}&type=agent_config", + cancellationToken); + + return new JsApiTicketInfo(cackeItem.Ticket, cackeItem.ExpiresIn); + } + + public async virtual Task GetTicketInfoAsync(CancellationToken cancellationToken = default) + { + var cacheKey = nameof(GetTicketInfoAsync); + var token = await WeChatWorkTokenProvider.GetTokenAsync(cancellationToken); + var cackeItem = await GetCacheItemAsync( + cacheKey, + $"/cgi-bin/get_jsapi_ticket?access_token={token.AccessToken}", + cancellationToken); + + return new JsApiTicketInfo(cackeItem.Ticket, cackeItem.ExpiresIn); + } + + public virtual JsApiSignatureData GenerateSignature(JsApiTicketInfo ticketInfo, string url, CancellationToken cancellationToken = default) + { + var nonce = JsApiTicketHelper.GenerateNonce(); + var timestamp = JsApiTicketHelper.GenerateTimestamp().ToString(); + var signature = JsApiTicketHelper.GenerateSignature(ticketInfo.Ticket, nonce, timestamp, url); + + return new JsApiSignatureData(nonce, timestamp, signature); + } + + protected async virtual Task GetCacheItemAsync( + string cacheKey, + string jsapiTicketUrl, + CancellationToken cancellationToken = default) + { + var cacheItem = await Cache.GetAsync(cacheKey, token: cancellationToken); + + if (cacheItem != null) + { + Logger.LogDebug($"Found JsApiTicket in the cache: {cacheKey}"); + return cacheItem; + } + + Logger.LogDebug($"Not found JsApiTicket in the cache, getting from the httpClient: {cacheKey}"); + + var client = HttpClientFactory.CreateClient(AbpWeChatWorkGlobalConsts.ApiClient); + + using var response = await client.GetAsync( + jsapiTicketUrl, + cancellationToken); + var ticketInfoResponse = await response.DeserializeObjectAsync(); + var ticketInfo = ticketInfoResponse.ToJsApiTicket(); + cacheItem = new JsApiTicketInfoCacheItem(ticketInfo.Ticket, ticketInfo.ExpiresIn); + + Logger.LogDebug($"Setting the cache item: {cacheKey}"); + + var cacheOptions = new DistributedCacheEntryOptions + { + // 设置绝对过期时间为Token有效期剩余的二分钟 + AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(ticketInfo.ExpiresIn - 100), + }; + + await Cache.SetAsync(cacheKey, cacheItem, cacheOptions, token: cancellationToken); + + Logger.LogDebug($"Finished setting the cache item: {cacheKey}"); + + return cacheItem; + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiSignatureData.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiSignatureData.cs new file mode 100644 index 000000000..8d7c054c7 --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiSignatureData.cs @@ -0,0 +1,13 @@ +namespace LINGYUN.Abp.WeChat.Work.JsSdk.Models; +public class JsApiSignatureData +{ + public string Nonce { get; } + public string Timestamp { get; } + public string Signature { get; } + public JsApiSignatureData(string nonce, string timestamp, string signature) + { + Nonce = nonce; + Timestamp = timestamp; + Signature = signature; + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfo.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfo.cs new file mode 100644 index 000000000..3b432736a --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfo.cs @@ -0,0 +1,22 @@ +namespace LINGYUN.Abp.WeChat.Work.JsSdk.Models; +public class JsApiTicketInfo +{ + /// + /// 生成签名所需的 jsapi_ticket,最长为512字节 + /// + public string Ticket { get; set; } + /// + /// 凭证的有效时间(秒) + /// + public int ExpiresIn { get; set; } + public JsApiTicketInfo() + { + + } + + public JsApiTicketInfo(string ticket, int expiresIn) + { + Ticket = ticket; + ExpiresIn = expiresIn; + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfoCacheItem.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfoCacheItem.cs new file mode 100644 index 000000000..eb48d8133 --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfoCacheItem.cs @@ -0,0 +1,19 @@ +namespace LINGYUN.Abp.WeChat.Work.JsSdk.Models; + +public class JsApiTicketInfoCacheItem +{ + public string Ticket { get; set; } + + public int ExpiresIn { get; set; } + + public JsApiTicketInfoCacheItem() + { + + } + + public JsApiTicketInfoCacheItem(string ticket, int expiresIn) + { + Ticket = ticket; + ExpiresIn = expiresIn; + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfoResponse.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfoResponse.cs new file mode 100644 index 000000000..e5c77dac3 --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfoResponse.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; +using System.Text.Json.Serialization; + +namespace LINGYUN.Abp.WeChat.Work.JsSdk.Models; + +public class JsApiTicketInfoResponse : WeChatWorkResponse +{ + /// + /// 生成签名所需的 jsapi_ticket,最长为512字节 + /// + [JsonProperty("ticket")] + [JsonPropertyName("ticket")] + public string Ticket { get; set; } + /// + /// 凭证的有效时间(秒) + /// + [JsonProperty("expires_in")] + [JsonPropertyName("expires_in")] + [System.Text.Json.Serialization.JsonConverter(typeof(NumberToStringConverter))] + public int ExpiresIn { get; set; } + + public JsApiTicketInfo ToJsApiTicket() + { + ThrowIfNotSuccess(); + return new JsApiTicketInfo(Ticket, ExpiresIn); + } +} diff --git a/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore.MySql/LY.MicroService.Applications.Single.EntityFrameworkCore.MySql.csproj b/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore.MySql/LY.MicroService.Applications.Single.EntityFrameworkCore.MySql.csproj index 37733ad57..cafad2948 100644 --- a/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore.MySql/LY.MicroService.Applications.Single.EntityFrameworkCore.MySql.csproj +++ b/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore.MySql/LY.MicroService.Applications.Single.EntityFrameworkCore.MySql.csproj @@ -12,7 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore.MySql/Migrations/20250813012035_Upgrade-Abp-Framework-To-9.3.1.Designer.cs b/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore.MySql/Migrations/20250813012035_Upgrade-Abp-Framework-To-9.3.1.Designer.cs new file mode 100644 index 000000000..503871227 --- /dev/null +++ b/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore.MySql/Migrations/20250813012035_Upgrade-Abp-Framework-To-9.3.1.Designer.cs @@ -0,0 +1,5470 @@ +// +using System; +using LY.MicroService.Applications.Single.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace LY.MicroService.Applications.Single.EntityFrameworkCore.MySql.Migrations +{ + [DbContext(typeof(SingleMigrationsDbContext))] + [Migration("20250813012035_Upgrade-Abp-Framework-To-9.3.1")] + partial class UpgradeAbpFrameworkTo931 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.MySql) + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("LINGYUN.Abp.DataProtectionManagement.EntityEnumInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DisplayName"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Name"); + + b.Property("PropertyInfoId") + .HasColumnType("char(36)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)") + .HasColumnName("Value"); + + b.HasKey("Id"); + + b.HasIndex("PropertyInfoId", "Name"); + + b.ToTable("AbpAuthEntityEnums", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.DataProtectionManagement.EntityPropertyInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DisplayName"); + + b.Property("JavaScriptType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("JavaScriptType"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Name"); + + b.Property("TypeFullName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("TypeFullName"); + + b.Property("TypeInfoId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TypeInfoId", "TypeFullName"); + + b.ToTable("AbpAuthEntityProperties", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.DataProtectionManagement.EntityTypeInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DisplayName"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsAuditEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Name"); + + b.Property("TypeFullName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("TypeFullName"); + + b.HasKey("Id"); + + b.HasIndex("TypeFullName"); + + b.ToTable("AbpAuthEntitites", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.DataProtectionManagement.OrganizationUnitEntityRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AccessedProperties") + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("AccessedProperties"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("EntityTypeFullName") + .HasColumnType("longtext"); + + b.Property("EntityTypeId") + .HasColumnType("char(36)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("FilterGroup") + .HasColumnType("longtext") + .HasColumnName("FilterGroup"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Operation") + .HasColumnType("int"); + + b.Property("OrgCode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("OrgCode"); + + b.Property("OrgId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("EntityTypeId"); + + b.ToTable("AbpAuthOrganizationUnitEntityRules", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.DataProtectionManagement.RoleEntityRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AccessedProperties") + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("AccessedProperties"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("EntityTypeFullName") + .HasColumnType("longtext"); + + b.Property("EntityTypeId") + .HasColumnType("char(36)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("FilterGroup") + .HasColumnType("longtext") + .HasColumnName("FilterGroup"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Operation") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.Property("RoleName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("RoleName"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("EntityTypeId"); + + b.ToTable("AbpAuthRoleEntityRules", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.DataProtectionManagement.SubjectStrategy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Strategy") + .HasColumnType("int"); + + b.Property("SubjectId") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("SubjectId"); + + b.Property("SubjectName") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("SubjectName"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AbpAuthSubjectStrategys", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Demo.Authors.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BirthDate") + .HasColumnType("datetime(6)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ShortBio") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Demo_Authors", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Demo.Books.Book", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AuthorId") + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Price") + .HasColumnType("float"); + + b.Property("PublishDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Demo_Books", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Demo.Books.BookAuth", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("EntityId") + .HasMaxLength(64) + .HasColumnType("char(64)") + .HasColumnName("EntityId"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("EntityType"); + + b.Property("OrganizationUnit") + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("OrganizationUnit"); + + b.Property("Role") + .HasMaxLength(32) + .HasColumnType("varchar(32)") + .HasColumnName("Role"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EntityId"); + + b.HasIndex("OrganizationUnit"); + + b.HasIndex("Role"); + + b.ToTable("Demo_BooksAuths", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Gdpr.GdprInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("Data"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("Provider"); + + b.Property("RequestId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("RequestId"); + + b.ToTable("AbpGdprInfos", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Gdpr.GdprRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("ReadyTime") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AbpGdprRequests", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.LocalizationManagement.Language", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("CultureName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("CultureName"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("DisplayName"); + + b.Property("Enable") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("TwoLetterISOLanguageName") + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("TwoLetterISOLanguageName"); + + b.Property("UiCultureName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("UiCultureName"); + + b.HasKey("Id"); + + b.HasIndex("CultureName"); + + b.ToTable("AbpLocalizationLanguages", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.LocalizationManagement.Resource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DefaultCultureName") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("DefaultCultureName"); + + b.Property("Description") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Description"); + + b.Property("DisplayName") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("DisplayName"); + + b.Property("Enable") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("Name"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("AbpLocalizationResources", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.LocalizationManagement.Text", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CultureName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("CultureName"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("Key"); + + b.Property("ResourceName") + .HasColumnType("longtext"); + + b.Property("Value") + .HasMaxLength(2048) + .HasColumnType("varchar(2048)") + .HasColumnName("Value"); + + b.HasKey("Id"); + + b.HasIndex("Key"); + + b.ToTable("AbpLocalizationTexts", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.MessageService.Chat.UserChatCard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property
+ {{ item.title }} +