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

接入飞书

parent 0ae3f35b
......@@ -12,6 +12,13 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.min.js"></script>
<!-- Google tag (gtag.js) -->
<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>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
......@@ -19,6 +26,11 @@
gtag('config', 'G-3PSXKB099C');
</script>
<script src="https://unpkg.com/vconsole/dist/vconsole.min.js"></script>
<script>
var vConsole = new window.VConsole();
</script>
</head>
<body>
<div id="root"></div>
......
......@@ -372,7 +372,7 @@ const DocumentTranslatorContent = ({ currentLanguage, onLanguageChange }) => {
const handlePaste = (e) => {
if (!checkAuthentication()) {
// For clipboard pastes, just redirect to login without saving
window.location.href = '/login';
// window.location.href = '/login';
return;
}
......
......@@ -8,16 +8,96 @@ import logo from '/assets/logo.png';
function Header() {
const { t } = useTranslation();
const navigate = useNavigate();
const [user, setUser] = useState(null);
const [user, setUser] = useState({});
const [avatarUrl, setAvatarUrl]= useState("");
const [menuOpen, setMenuOpen] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
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
useEffect(() => {
const checkUser = () => {
try {
const userData = localStorage.getItem('user');
const userData = localStorage.getItem('userInfo');
if (userData) {
setUser(JSON.parse(userData));
} else {
......@@ -54,10 +134,10 @@ function Header() {
}, [menuRef]);
const handleLogout = () => {
localStorage.removeItem('user');
setUser(null);
navigate('/login');
setMobileMenuOpen(false);
// localStorage.removeItem('user');
// setUser(null);
// navigate('/login');
// setMobileMenuOpen(false);
};
const toggleMobileMenu = () => {
......@@ -121,6 +201,11 @@ function Header() {
};
}, [mobileMenuOpen]);
// const avatarUrl = JSON.parse(localStorage.getItem('userInfo') || '{}').avatarUrl || '';
// useEffect(()=>{
// const avatarUrl = JSON.parse(localStorage.getItem('userInfo') || '{}').avatarUrl || '';
// },[user])
return (
<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>
......@@ -229,7 +314,7 @@ function Header() {
className="flex items-center space-x-3 focus:outline-none"
>
<img
src="https://th.bing.com/th/id/OIP.nl9qIl6NYdJmv6jeMd8H7gAAAA?rs=1&pid=ImgDetMain&cb=idpwebpc2"
src={avatarUrl}
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"
/>
......@@ -247,12 +332,12 @@ function Header() {
)}
</div>
) : (
<NavLink
to="/login"
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"
>
{t('login')}
</NavLink>
<img
src={avatarUrl}
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"
/>
)}
</div>
......@@ -341,19 +426,7 @@ function Header() {
>
{t('image-tools.title')}
</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
to="/document-translator"
className={({isActive}) =>
......@@ -396,17 +469,7 @@ function Header() {
</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>
</div>
</div>
......
......@@ -7,12 +7,12 @@ import { Navigate, useLocation } from 'react-router-dom';
*/
const PrivateRoute = ({ children }) => {
const location = useLocation();
const user = localStorage.getItem('user');
if (!user) {
// 记录跳转前的路径,登录后可重定向回来
sessionStorage.setItem('redirectAfterLogin', location.pathname + location.search);
return <Navigate to="/login" replace />;
}
const user = localStorage.getItem('userInfo');
// if (!user) {
// // 记录跳转前的路径,登录后可重定向回来
// sessionStorage.setItem('redirectAfterLogin', location.pathname + location.search);
// return <Navigate to="/login" replace />;
// }
return children;
};
......
// src/pages/Login.jsx
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from '../js/i18n';
import '../styles/Login.css';
const Login = () => {
const navigate = useNavigate();
const { t } = useTranslation();
// 新增:用户名密码登录相关状态
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({
env: 'xingzhi-authing-5gkb8pggc4cf2edf',
});
try {
if (!app) {
setLoginError('CloudBase SDK 未初始化');
setLoginLoading(false);
const [userInfo, setUserInfo] = useState(null);
const lang = window.navigator.language;
useEffect(() => {
const apiAuth = async () => {
console.log("start apiAuth");
if (!window.h5sdk) {
console.log("invalid h5sdk");
alert("please open in feishu");
return;
}
// 0. 先进行匿名登录,避免 "you can't request without auth" 报错
const auth = app.auth();
await auth.signInAnonymously();
// 1. 调用云函数 login 校验用户名密码,返回 ticket
const res = await app.callFunction({
name: 'login',
data: {
username,
password,
},
});
const { code } = res.result || {};
if (code == 200) {
localStorage.setItem('user', JSON.stringify({ username }));
window.location.href = '/';
} else {
setLoginError('用户名或密码错误');
}
} catch (err) {
setLoginError(err.message || '用户名或密码错误');
setLoginLoading(false);
}
};
// 记住密码功能:初始化时自动填充
useEffect(() => {
const saved = localStorage.getItem('rememberedLogin');
if (saved) {
const url = encodeURIComponent(location.href.split("#")[0]);
console.log("接入方前端将需要鉴权的url发给接入方服务端,url为:", url);
try {
const { username: savedUser, password: savedPass } = JSON.parse(saved);
setUsername(savedUser || '');
setPassword(savedPass || '');
setRememberMe(true);
} catch {}
}
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)}`);
setUserInfo(res.userInfo);
localStorage.setItem('userInfo', JSON.stringify(res.userInfo));
},
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();
}, []);
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 (
// <div>
// <div id="img_div">
// <img src={userInfo.avatarUrl} alt="avatar" style={{ width: '100%' }} />
// </div>
// <h2 id="hello_text_name">{name}</h2>
// <p id="hello_text_welcome">{welcomeText}</p>
// </div>
<></>
);
};
return (
<div className="login-container">
<div className="login-card">
<h1 className="login-title">{t('login')}</h1>
<p className="login-subtitle">
{t('loginSubtitle', '欢迎使用 AI 工具箱,请登录以获得完整体验')}
</p>
<div className="login-options">
{/* 腾讯云开发用户名密码登录表单 */}
<form className="cloudbase-login-form" onSubmit={handleCloudbaseLogin}>
<input
type="text"
placeholder="用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
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>
{/* <h1>Feishu 鉴权 Demo</h1>
{renderUserInfo()} */}
</div>
);
};
......
......@@ -5,5 +5,5 @@ import sitemap from 'vite-plugin-sitemap';
export default defineConfig({
server: {
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