feat: 复制几个固定页面到模板中

This commit is contained in:
Pan Qiancheng 2025-10-23 16:21:17 +08:00
parent fb9489a2f5
commit d7b34fb3f6
28 changed files with 541 additions and 4 deletions

View File

@ -0,0 +1,62 @@
import { EntityDict } from "@project/oak-app-domain";
import assert from "assert";
import { RowWithActions } from "oak-frontend-base";
import { Cache } from "@oak-frontend-base/features/cache";
import { generateNewId } from "oak-domain/lib/utils/uuid";
import { Token } from "@oak-general-business/features/token";
import { FeatureDict } from "@project/features";
export default OakComponent({
entity: 'oauthProvider',
isList: true,
projection: {
name: 1,
type: 1,
logo: 1,
authorizationEndpoint: 1,
redirectUri: 1,
clientId: 1,
},
filters: [{
filter() {
const application = this.features.application.getApplication();
return {
systemId: application?.systemId!,
ableState: "enabled",
};
},
}],
formData({ data }) {
return {
list: data,
};
},
data: {
loading: false
},
properties: {},
methods: {
onLogin(provider: RowWithActions<EntityDict, 'oauthProvider'>) {
this.setLoading(true);
const { name, type, redirectUri, clientId } = provider;
assert(redirectUri, 'Redirect URI is required for OAuth login');
assert(clientId, 'Client ID is required for OAuth login');
this.features.aspect.createOAuthState({
providerId: provider.id!,
type: "login",
userId: this.features.token.getUserId(true),
}).then(state => {
const target = `${provider.authorizationEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri!)}&state=${state}`;
// 跳转第三方认证页面
window.location.href = target;
})
console.log('Logging in with provider:', provider);
},
setLoading(loading: boolean) {
this.setState({ loading });
}
}
});

View File

@ -0,0 +1,3 @@
{
"waiting": "等待中..."
}

View File

@ -0,0 +1,152 @@
.providerContainer {
width: 100%;
margin-top: 24px;
padding: 20px 0;
}
.loadingWrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
}
.loadingDots {
display: flex;
gap: 8px;
margin-bottom: 16px;
span {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #1890ff;
animation: loadingDot 1.4s infinite ease-in-out both;
&:nth-child(1) {
animation-delay: -0.32s;
}
&:nth-child(2) {
animation-delay: -0.16s;
}
}
}
@keyframes loadingDot {
0%, 80%, 100% {
transform: scale(0.6);
opacity: 0.3;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.loadingText {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
}
.title {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
text-align: center;
margin-bottom: 24px;
position: relative;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
width: 30%;
height: 1px;
background-color: rgba(0, 0, 0, 0.1);
}
&::before {
left: 0;
}
&::after {
right: 0;
}
}
.providerList {
display: flex;
justify-content: center;
align-items: center;
gap: 32px;
flex-wrap: wrap;
}
.providerItem {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: translateY(-4px);
.logoWrapper {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
}
}
.logoWrapper {
width: 56px;
height: 56px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #e8e8e8;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
overflow: hidden;
margin-bottom: 8px;
&:hover {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
}
.logo {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.logoPlaceholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: 600;
color: #fff;
}
.providerName {
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
text-align: center;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.3s ease;
.providerItem:hover & {
color: #1890ff;
}
}

View File

@ -0,0 +1,77 @@
import React from 'react';
import { EntityDict } from '@project/oak-app-domain';
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import { Tooltip } from 'antd';
import Styles from './styles.module.less';
const ProviderList = (
props: WebComponentProps<
EntityDict,
'oauthProvider',
true,
{
list: RowWithActions<EntityDict, 'oauthProvider'>[];
loading: boolean;
},
{
onLogin: (provider: RowWithActions<EntityDict, 'oauthProvider'>) => void
}
>
) => {
const { list, loading } = props.data;
const { t, onLogin } = props.methods;
if (loading) {
return (
<div className={Styles.providerContainer}>
<div className={Styles.loadingWrapper}>
<div className={Styles.loadingDots}>
<span></span>
<span></span>
<span></span>
</div>
<div className={Styles.loadingText}>{t("waiting")}</div>
</div>
</div>
);
}
return (
<div className={Styles.providerContainer}>
{list && list.length > 0 && (
<>
<div className={Styles.title}></div>
<div className={Styles.providerList}>
{list.map((item) => {
return (
<Tooltip key={item.id} title={item.name} placement="bottom">
<div
className={Styles.providerItem}
onClick={() => onLogin(item)}
>
<div className={Styles.logoWrapper}>
{item.logo ? (
<img
src={item.logo}
alt={item.name}
className={Styles.logo}
/>
) : (
<div className={Styles.logoPlaceholder}>
{item.name?.charAt(0).toUpperCase()}
</div>
)}
</div>
<div className={Styles.providerName}>{item.name}</div>
</div>
</Tooltip>
);
})}
</div>
</>
)}
</div>
);
};
export default ProviderList;

View File

@ -0,0 +1,6 @@
export default OakComponent({
// Virtual Component
isList: false,
filters: [],
properties:{}
});

View File

@ -0,0 +1,3 @@
{
}

View File

@ -0,0 +1,7 @@
.id {
font-size: 18px;
}
.item {
font-size: 18px;
}

View File

@ -0,0 +1,22 @@
import React from 'react';
import { EntityDict } from '@project/oak-app-domain';
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import Styles from './styles.module.less';
import ProviderList from './ProviderList';
const ByOauth = (
props: WebComponentProps<
EntityDict,
keyof EntityDict,
false,
{
// virtual
}
>
) => {
const { oakFullpath } = props.data;
return (<ProviderList oakPath={`oauthProviders:providers`}/>);
};
export default ByOauth;

View File

@ -11,12 +11,25 @@ export default OakComponent({
onLoggedInMp() {
const { onLoggedIn } = this.props;
onLoggedIn && onLoggedIn();
}
},
dispose : null as (() => void) | null,
},
methods: {
logout() {
return this.features.token.logout();
}
},
lifetimes: {
ready() {
const dispose = this.features.token.subscribe(() => {
this.props.onLoggedIn && this.props.onLoggedIn();
});
this.setState({ dispose });
},
detached() {
this.state.dispose && this.state.dispose();
}
},
features: ['token'],
});

View File

@ -3,4 +3,10 @@
display: flex;
align-items: center;
justify-content: center;
}
.login {
display: flex;
flex-direction: column;
gap: 32px;
}

View File

@ -4,6 +4,7 @@ import { WebComponentProps } from 'oak-frontend-base';
import { Button, Result } from 'antd';
import { EntityDict } from '@project/oak-app-domain';
import Styles from './web.pc.module.less';
import ByOauth from '@project/components/login/byOauth';
export default function render(props: WebComponentProps<EntityDict, 'user', false, {
loggedIn: boolean;
@ -15,9 +16,12 @@ export default function render(props: WebComponentProps<EntityDict, 'user', fals
if (!loggedIn) {
return (
<GeneralLogin
oakPath="$$general-login"
/>
<div className={Styles.login}>
<GeneralLogin
oakPath="$$general-login"
/>
<ByOauth oakPath={`#login`} />
</div>
);
}

View File

@ -0,0 +1,6 @@
{
"enablePullDownRefresh": false,
"usingComponents": {
"login": "@project/components/login/index"
}
}

View File

@ -0,0 +1,7 @@
.container {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}

View File

@ -0,0 +1,36 @@
export default OakComponent({
properties: {
redirect: "",
},
data: {
redirectUrl: "",
},
methods: {
onLoggedIn() {
const { redirectUrl } = this.state;
console.log('redirecting to', redirectUrl);
if (redirectUrl) {
this.features.navigator.navigateTo({ url: redirectUrl }, undefined, true);
}
},
loadSearchParams() {
const searchParams = new URLSearchParams(window.location.search);
const encoded = searchParams.get('redirect') || '';
const redirectUrl = encoded ? decodeURIComponent(atob(encoded)) : '';
console.log('Loaded redirect URL from search params:', redirectUrl);
this.setState({
redirectUrl: redirectUrl,
});
},
},
lifetimes: {
ready() {
this.loadSearchParams();
if (this.features.token.getUserId(true)) {
this.onLoggedIn();
}
},
}
});

View File

@ -0,0 +1,3 @@
<login
oakPath="$$login"
/>

View File

@ -0,0 +1,3 @@
{
"pageTitle": "登录"
}

View File

@ -0,0 +1,7 @@
.container {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
}

View File

@ -0,0 +1,6 @@
export default OakComponent({
// Virtual Component
isList: false,
filters: [],
properties:{}
});

View File

@ -0,0 +1,3 @@
{
}

View File

@ -0,0 +1,7 @@
.id {
font-size: 18px;
}
.item {
font-size: 18px;
}

View File

@ -0,0 +1,22 @@
import React from 'react';
import { EntityDict } from '@project/oak-app-domain';
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import Styles from './styles.module.less';
import Auth from "oak-general-business/es/components/login/oauth/authorize"
const Authorize = (
props: WebComponentProps<
EntityDict,
keyof EntityDict,
false,
{
// virtual
}
>
) => {
return <Auth
oakPath={`#Authorize`}
/>;
};
export default Authorize;

View File

@ -0,0 +1,7 @@
.container {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 22px;
}

View File

@ -0,0 +1,24 @@
import React from 'react';
import Login from '@project/components/login';
import Styles from './pc.module.less';
import { WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '@project/oak-app-domain';
export default function Render(props: WebComponentProps<
EntityDict,
"user",
false,
{},
{
onLoggedIn: () => void;
}
>) {
const { onLoggedIn } = props.methods;
return (
<div className={Styles.container}>
<Login onLoggedIn={onLoggedIn} />
</div>
)
}

View File

@ -0,0 +1,11 @@
import React from 'react';
import Login from '@project/components/login';
import Styles from './pc.module.less';
export default function Render() {
return (
<div className={Styles.container}>
<Login />
</div>
)
}

View File

@ -0,0 +1,6 @@
export default OakComponent({
// Virtual Component
isList: false,
filters: [],
properties:{}
});

View File

@ -0,0 +1,3 @@
{
}

View File

@ -0,0 +1,7 @@
.id {
font-size: 18px;
}
.item {
font-size: 18px;
}

View File

@ -0,0 +1,24 @@
import React from 'react';
import { EntityDict } from '@project/oak-app-domain';
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import Styles from './styles.module.less';
import OAuth from "oak-general-business/es/components/oauth"
const Oauth = (
props: WebComponentProps<
EntityDict,
keyof EntityDict,
false,
{
// virtual
}
>
) => {
const { oakFullpath } = props.data;
return <OAuth
oakPath={`#OAuth`}
></OAuth>
};
export default Oauth;