Commit 22fbf6c1 authored by 青山's avatar 青山

接入飞书

parent 0ae3f35b
...@@ -12,6 +12,13 @@ ...@@ -12,6 +12,13 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.min.js"></script>
<!-- Google tag (gtag.js) --> <!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-3PSXKB099C"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=G-3PSXKB099C"></script>
<!-- 飞书SDK -->
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<!-- 引入 JSSDK -->
<!-- JS 文件版本在升级功能时地址会变化,如有需要(比如使用新增的 API),请重新引用「网页应用开发指南」中的JSSDK链接,确保你当前使用的JSSDK版本是最新的。-->
<script type="text/javascript" src="https://lf1-cdn-tos.bytegoofy.com/goofy/lark/op/h5-js-sdk-1.5.16.js"></script>
<script> <script>
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);} function gtag(){dataLayer.push(arguments);}
...@@ -19,6 +26,11 @@ ...@@ -19,6 +26,11 @@
gtag('config', 'G-3PSXKB099C'); gtag('config', 'G-3PSXKB099C');
</script> </script>
<script src="https://unpkg.com/vconsole/dist/vconsole.min.js"></script>
<script>
var vConsole = new window.VConsole();
</script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
......
...@@ -372,7 +372,7 @@ const DocumentTranslatorContent = ({ currentLanguage, onLanguageChange }) => { ...@@ -372,7 +372,7 @@ const DocumentTranslatorContent = ({ currentLanguage, onLanguageChange }) => {
const handlePaste = (e) => { const handlePaste = (e) => {
if (!checkAuthentication()) { if (!checkAuthentication()) {
// For clipboard pastes, just redirect to login without saving // For clipboard pastes, just redirect to login without saving
window.location.href = '/login'; // window.location.href = '/login';
return; return;
} }
......
...@@ -8,16 +8,96 @@ import logo from '/assets/logo.png'; ...@@ -8,16 +8,96 @@ import logo from '/assets/logo.png';
function Header() { function Header() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [user, setUser] = useState(null); const [user, setUser] = useState({});
const [avatarUrl, setAvatarUrl]= useState("");
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const menuRef = useRef(null); const menuRef = useRef(null);
useEffect(() => {
const lang = window.navigator.language;
const apiAuth = async () => {
console.log("start apiAuth");
if (!window.h5sdk) {
console.log("invalid h5sdk");
alert("please open in feishu");
return;
}
const url = encodeURIComponent(location.href.split("#")[0]);
console.log("接入方前端将需要鉴权的url发给接入方服务端,url为:", url);
try {
const response = await fetch(`/get_config_parameters?url=${url}`);
const res = await response.json();
console.log(
"接入方服务端返回给接入方前端的结果(前端调用config接口的所需参数):", res
);
window.h5sdk.error((err) => {
console.error("h5sdk error:", JSON.stringify(err));
});
window.h5sdk.config({
appId: res.appid,
timestamp: res.timestamp,
nonceStr: res.noncestr,
signature: res.signature,
jsApiList: [],
onSuccess: (res) => {
console.log(`config success: ${JSON.stringify(res)}`);
},
onFail: (err) => {
console.error(`config failed: ${JSON.stringify(err)}`);
},
});
window.h5sdk.ready(() => {
// 获取用户信息
window.tt.getUserInfo({
success: (res) => {
console.log(`getUserInfo success: ${JSON.stringify(res)}`);
setUser(res.userInfo);
localStorage.setItem('userInfo', JSON.stringify(res.userInfo));
setAvatarUrl(res.userInfo.avatarUrl)
},
fail: (err) => {
console.log(`getUserInfo failed:`, JSON.stringify(err));
},
});
// 显示Toast
window.tt.showToast({
title: "鉴权成功",
icon: "success",
duration: 3000,
success(res) {
console.log("showToast 调用成功", res.errMsg);
},
fail(res) {
// console.log("showToast 调用失败", res.errMsg);
},
complete(res) {
// console.log("showToast 调用结束", res.errMsg);
},
});
});
} catch (e) {
console.error("fetch error:", e);
}
};
apiAuth();
}, []);
// Check for user on component mount and localStorage changes // Check for user on component mount and localStorage changes
useEffect(() => { useEffect(() => {
const checkUser = () => { const checkUser = () => {
try { try {
const userData = localStorage.getItem('user'); const userData = localStorage.getItem('userInfo');
if (userData) { if (userData) {
setUser(JSON.parse(userData)); setUser(JSON.parse(userData));
} else { } else {
...@@ -54,10 +134,10 @@ function Header() { ...@@ -54,10 +134,10 @@ function Header() {
}, [menuRef]); }, [menuRef]);
const handleLogout = () => { const handleLogout = () => {
localStorage.removeItem('user'); // localStorage.removeItem('user');
setUser(null); // setUser(null);
navigate('/login'); // navigate('/login');
setMobileMenuOpen(false); // setMobileMenuOpen(false);
}; };
const toggleMobileMenu = () => { const toggleMobileMenu = () => {
...@@ -121,6 +201,11 @@ function Header() { ...@@ -121,6 +201,11 @@ function Header() {
}; };
}, [mobileMenuOpen]); }, [mobileMenuOpen]);
// const avatarUrl = JSON.parse(localStorage.getItem('userInfo') || '{}').avatarUrl || '';
// useEffect(()=>{
// const avatarUrl = JSON.parse(localStorage.getItem('userInfo') || '{}').avatarUrl || '';
// },[user])
return ( return (
<header className="fixed top-0 left-0 right-0 z-50"> <header className="fixed top-0 left-0 right-0 z-50">
<div className="absolute inset-0 bg-white/70 backdrop-blur-lg border-b border-gray-100"></div> <div className="absolute inset-0 bg-white/70 backdrop-blur-lg border-b border-gray-100"></div>
...@@ -229,7 +314,7 @@ function Header() { ...@@ -229,7 +314,7 @@ function Header() {
className="flex items-center space-x-3 focus:outline-none" className="flex items-center space-x-3 focus:outline-none"
> >
<img <img
src="https://th.bing.com/th/id/OIP.nl9qIl6NYdJmv6jeMd8H7gAAAA?rs=1&pid=ImgDetMain&cb=idpwebpc2" src={avatarUrl}
alt="User Avatar" alt="User Avatar"
className="w-8 h-8 sm:w-10 sm:h-10 rounded-full ring-2 ring-offset-2 ring-indigo-500 transition transform hover:scale-105" className="w-8 h-8 sm:w-10 sm:h-10 rounded-full ring-2 ring-offset-2 ring-indigo-500 transition transform hover:scale-105"
/> />
...@@ -247,12 +332,12 @@ function Header() { ...@@ -247,12 +332,12 @@ function Header() {
)} )}
</div> </div>
) : ( ) : (
<NavLink
to="/login" <img
className="hidden sm:inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-indigo-600 hover:bg-indigo-700 transition-colors duration-200 shadow-sm hover:shadow-md" src={avatarUrl}
> alt="User Avatar"
{t('login')} className="w-8 h-8 sm:w-10 sm:h-10 rounded-full ring-2 ring-offset-2 ring-indigo-500 transition transform hover:scale-105"
</NavLink> />
)} )}
</div> </div>
...@@ -341,19 +426,7 @@ function Header() { ...@@ -341,19 +426,7 @@ function Header() {
> >
{t('image-tools.title')} {t('image-tools.title')}
</NavLink> </NavLink>
<NavLink
to="/translator"
className={({isActive}) =>
`block px-4 py-3 rounded-lg text-base font-medium transition-colors duration-200 ${
isActive
? 'bg-indigo-50 text-indigo-600'
: 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600'
}`
}
onClick={() => setMobileMenuOpen(false)}
>
翻译工具
</NavLink>
<NavLink <NavLink
to="/document-translator" to="/document-translator"
className={({isActive}) => className={({isActive}) =>
...@@ -396,17 +469,7 @@ function Header() { ...@@ -396,17 +469,7 @@ function Header() {
</div> </div>
</div> </div>
<div className="border-t border-gray-200 p-4">
{!user && (
<NavLink
to="/login"
className="flex items-center justify-center px-4 py-2 border border-transparent text-base font-medium rounded-lg text-white bg-indigo-600 hover:bg-indigo-700 transition-colors duration-200 shadow-sm hover:shadow-md w-full"
onClick={() => setMobileMenuOpen(false)}
>
{t('login')}
</NavLink>
)}
</div>
</nav> </nav>
</div> </div>
</div> </div>
......
...@@ -7,12 +7,12 @@ import { Navigate, useLocation } from 'react-router-dom'; ...@@ -7,12 +7,12 @@ import { Navigate, useLocation } from 'react-router-dom';
*/ */
const PrivateRoute = ({ children }) => { const PrivateRoute = ({ children }) => {
const location = useLocation(); const location = useLocation();
const user = localStorage.getItem('user'); const user = localStorage.getItem('userInfo');
if (!user) { // if (!user) {
// 记录跳转前的路径,登录后可重定向回来 // // 记录跳转前的路径,登录后可重定向回来
sessionStorage.setItem('redirectAfterLogin', location.pathname + location.search); // sessionStorage.setItem('redirectAfterLogin', location.pathname + location.search);
return <Navigate to="/login" replace />; // return <Navigate to="/login" replace />;
} // }
return children; return children;
}; };
......
// src/pages/Login.jsx
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from '../js/i18n';
import '../styles/Login.css';
const Login = () => { const Login = () => {
const navigate = useNavigate(); const [userInfo, setUserInfo] = useState(null);
const { t } = useTranslation(); const lang = window.navigator.language;
// 新增:用户名密码登录相关状态
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loginLoading, setLoginLoading] = useState(false);
const [loginError, setLoginError] = useState('');
const [rememberMe, setRememberMe] = useState(false);
// 新增:腾讯云开发用户名密码登录
const handleCloudbaseLogin = async (e) => {
e.preventDefault();
setLoginError('');
setLoginLoading(true);
// 记住密码逻辑
if (rememberMe) {
localStorage.setItem('rememberedLogin', JSON.stringify({ username, password }));
} else {
localStorage.removeItem('rememberedLogin');
}
const app = cloudbase.init({ useEffect(() => {
env: 'xingzhi-authing-5gkb8pggc4cf2edf', const apiAuth = async () => {
}); console.log("start apiAuth");
try { if (!window.h5sdk) {
if (!app) { console.log("invalid h5sdk");
setLoginError('CloudBase SDK 未初始化'); alert("please open in feishu");
setLoginLoading(false);
return; return;
} }
// 0. 先进行匿名登录,避免 "you can't request without auth" 报错
const auth = app.auth(); const url = encodeURIComponent(location.href.split("#")[0]);
await auth.signInAnonymously(); console.log("接入方前端将需要鉴权的url发给接入方服务端,url为:", url);
// 1. 调用云函数 login 校验用户名密码,返回 ticket
const res = await app.callFunction({ try {
name: 'login', const response = await fetch(`/get_config_parameters?url=${url}`);
data: { const res = await response.json();
username,
password, console.log(
"接入方服务端返回给接入方前端的结果(前端调用config接口的所需参数):", res
);
window.h5sdk.error((err) => {
console.error("h5sdk error:", JSON.stringify(err));
});
window.h5sdk.config({
appId: res.appid,
timestamp: res.timestamp,
nonceStr: res.noncestr,
signature: res.signature,
jsApiList: [],
onSuccess: (res) => {
console.log(`config success: ${JSON.stringify(res)}`);
},
onFail: (err) => {
console.error(`config failed: ${JSON.stringify(err)}`);
}, },
}); });
const { code } = res.result || {};
if (code == 200) { window.h5sdk.ready(() => {
localStorage.setItem('user', JSON.stringify({ username })); // 获取用户信息
window.location.href = '/'; window.tt.getUserInfo({
} else { success: (res) => {
setLoginError('用户名或密码错误'); console.log(`getUserInfo success: ${JSON.stringify(res)}`);
} setUserInfo(res.userInfo);
} catch (err) { localStorage.setItem('userInfo', JSON.stringify(res.userInfo));
setLoginError(err.message || '用户名或密码错误'); },
setLoginLoading(false); fail: (err) => {
console.log(`getUserInfo failed:`, JSON.stringify(err));
},
});
// 显示Toast
window.tt.showToast({
title: "鉴权成功",
icon: "success",
duration: 3000,
success(res) {
console.log("showToast 调用成功", res.errMsg);
},
fail(res) {
// console.log("showToast 调用失败", res.errMsg);
},
complete(res) {
// console.log("showToast 调用结束", res.errMsg);
},
});
});
} catch (e) {
console.error("fetch error:", e);
} }
}; };
// 记住密码功能:初始化时自动填充 apiAuth();
useEffect(() => {
const saved = localStorage.getItem('rememberedLogin');
if (saved) {
try {
const { username: savedUser, password: savedPass } = JSON.parse(saved);
setUsername(savedUser || '');
setPassword(savedPass || '');
setRememberMe(true);
} catch {}
}
}, []); }, []);
const renderUserInfo = () => {
if (!userInfo) return null;
const name =
lang === 'zh_CN' || lang === 'zh-CN'
? userInfo.nickName
: userInfo.i18nName?.en_us;
const welcomeText =
lang === 'zh_CN' || lang === 'zh-CN'
? '欢迎使用飞书'
: 'Welcome to Feishu';
return ( return (
<div className="login-container"> // <div>
<div className="login-card"> // <div id="img_div">
<h1 className="login-title">{t('login')}</h1> // <img src={userInfo.avatarUrl} alt="avatar" style={{ width: '100%' }} />
<p className="login-subtitle"> // </div>
{t('loginSubtitle', '欢迎使用 AI 工具箱,请登录以获得完整体验')} // <h2 id="hello_text_name">{name}</h2>
</p> // <p id="hello_text_welcome">{welcomeText}</p>
<div className="login-options"> // </div>
{/* 腾讯云开发用户名密码登录表单 */} <></>
<form className="cloudbase-login-form" onSubmit={handleCloudbaseLogin}> );
<input };
type="text"
placeholder="用户名" return (
value={username} <div>
onChange={(e) => setUsername(e.target.value)} {/* <h1>Feishu 鉴权 Demo</h1>
required {renderUserInfo()} */}
className="login-input enhanced-input"
autoComplete="username"
/>
<input
type="password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="login-input enhanced-input"
autoComplete="current-password"
/>
<label style={{ display: 'flex', alignItems: 'center', marginBottom: '0.5rem', fontSize: '0.98rem', color: '#6B7280', userSelect: 'none' }}>
<input
type="checkbox"
checked={rememberMe}
onChange={e => setRememberMe(e.target.checked)}
style={{ marginRight: '8px', accentColor: '#6366f1' }}
/>
记住密码
</label>
<button
type="submit"
className="login-btn enhanced-btn"
disabled={loginLoading}
>
{loginLoading ? '登录中...' : '用户名密码登录'}
</button>
{loginError && <div className="login-error">{loginError}</div>}
</form>
</div>
</div>
</div> </div>
); );
}; };
......
...@@ -5,5 +5,5 @@ import sitemap from 'vite-plugin-sitemap'; ...@@ -5,5 +5,5 @@ import sitemap from 'vite-plugin-sitemap';
export default defineConfig({ export default defineConfig({
server: { server: {
port: 3000 port: 3000
} },
}) })
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment