Sfoglia il codice sorgente

feat(widget): 重构登录组件为无 UI 的 headless SDK

master
fengsilin 3 settimane fa
parent
commit
79f8a18cef
10 ha cambiato i file con 163 aggiunte e 605 eliminazioni
  1. +100
    -171
      docs/login-widget-guide.md
  2. +7
    -2
      main.go
  3. +1
    -40
      web/dist/static/login-widget.js
  4. +0
    -83
      web/widget/LoginForm.jsx
  5. +0
    -109
      web/widget/bun.lock
  6. +53
    -0
      web/widget/index.js
  7. +0
    -71
      web/widget/index.jsx
  8. +1
    -7
      web/widget/package.json
  9. +0
    -115
      web/widget/styles.css
  10. +1
    -7
      web/widget/vite.config.js

+ 100
- 171
docs/login-widget-guide.md Vedi File

@@ -2,231 +2,160 @@

## 概述

登录 Widget 是一个可嵌入到任何网页的登录表单组件。通过一行 `<script>` 标签引入,即可在你的网站上展示 New API 的登录界面。用户登录成功后,通过回调函数返回用户数据,由嵌入方自行处理后续逻辑
登录 Widget 是一个轻量 Headless SDK(0.8KB),通过 `login()` 函数调用 New API 登录接口。不注入任何 DOM 或样式,你完全控制 UI

**当前版本仅支持密码登录。**
**仅支持密码登录。**

---

## 快速开始

### 1. 引入脚本

在你的 HTML 页面中添加 `<script>` 标签,`src` 指向你的 New API 服务地址:

```html
<script src="https://your-domain.com/static/login-widget.js"></script>
```

### 2. 创建容器

在页面中放置一个 DOM 元素作为 Widget 的挂载点:

```html
<div id="login-container"></div>
```

### 3. 初始化 Widget

在脚本加载后调用 `NewApiLoginWidget.init()`:

```html
<script src="https://lancerouter.ai/static/login-widget.js"></script>
<script>
var widget = NewApiLoginWidget.init({
container: '#login-container',
apiUrl: 'https://your-domain.com',
onSuccess: function(data) {
console.log('登录成功', data);
// 在这里处理登录成功逻辑
},
onError: function(err) {
console.log('登录失败', err.message);
}
const result = await NewApiLoginWidget.login({
apiUrl: 'https://lancerouter.ai',
username: 'user',
password: 'pass'
});
</script>
```

### 完整示例

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>我的网站</title>
</head>
<body>
<h1>欢迎</h1>

<!-- 登录 Widget 挂载点 -->
<div id="login-container"></div>

<!-- 引入 Widget 脚本 -->
<script src="https://your-domain.com/static/login-widget.js"></script>

<!-- 初始化 -->
<script>
var widget = NewApiLoginWidget.init({
container: '#login-container',
apiUrl: 'https://your-domain.com',
onSuccess: function(data) {
// 登录成功,保存用户信息
localStorage.setItem('user', JSON.stringify(data));
alert('欢迎, ' + data.username);
},
onError: function(err) {
alert('登录失败: ' + err.message);
}
});
</script>
</body>
</html>
if (result.success) {
console.log('登录成功', result.data);
} else {
console.log('登录失败', result.message);
}
</script>
```

---

## API 参考

### `NewApiLoginWidget.init(options)`

初始化并渲染登录表单。

#### 参数
### `NewApiLoginWidget.login(options)`

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `container` | `string` 或 `Element` | 是 | 挂载点,支持 CSS 选择器字符串或 DOM 元素 |
| `apiUrl` | `string` | 是 | New API 服务地址,如 `https://api.example.com` |
| `onSuccess` | `function` | 否 | 登录成功回调,参数为用户数据对象 |
| `onError` | `function` | 否 | 登录失败回调,参数为 `{ message: string }` |
| `theme` | `string` | 否 | 主题,可选 `'light'`(默认)或 `'dark'` |
| `apiUrl` | `string` | 是 | New API 服务地址 |
| `username` | `string` | 是 | 用户名或邮箱 |
| `password` | `string` | 是 | 密码 |

#### 返回值
**返回值:** `Promise<{ success: boolean, data?: object, message?: string }>`

返回一个对象,包含以下方法
成功:

| 方法 | 说明 |
|------|------|
| `destroy()` | 卸载 Widget,清除 DOM |
```json
{
"success": true,
"data": {
"id": 1,
"username": "user",
"display_name": "用户",
"role": 1,
"status": 1,
"group": "default"
}
}
```

#### onSuccess 回调数据
失败:

```json
{
"id": 1,
"username": "user",
"display_name": "用户",
"role": 1,
"status": 1,
"group": "default"
"success": false,
"message": "用户名或密码错误,或用户已被封禁"
}
```

### data 字段

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | number | 用户 ID |
| `id` | number | 用户 ID,调用其他 API 时需要 |
| `username` | string | 用户名 |
| `display_name` | string | 显示名称 |
| `role` | number | 角色(1=普通用户,10+=管理员,100+=超理员) |
| `role` | number | 角色(1=普通,10+=管理员,100+=超管) |
| `status` | number | 状态 |
| `group` | string | 用户组 |

---

## 主题
## 登录后调用其他 API

### Light(默认)
登录成功后浏览器会自动存储 session cookie,后续请求带 `credentials: 'include'` 和 `New-Api-User` 头即可。

```js
NewApiLoginWidget.init({
container: '#login',
apiUrl: 'https://your-domain.com',
// theme: 'light' // 默认就是 light,可省略
});
```

### Dark
### 获取用户信息

```js
NewApiLoginWidget.init({
container: '#login',
apiUrl: 'https://your-domain.com',
theme: 'dark'
const res = await fetch('https://lancerouter.ai/api/user/self', {
credentials: 'include',
headers: { 'New-Api-User': String(result.data.id) }
});
const data = await res.json();
```

---

## 销毁 Widget

当不再需要登录表单时(如登录成功后),调用 `destroy()` 移除:
### 创建 API Token

```js
var widget = NewApiLoginWidget.init({ ... });

// 登录成功后销毁
widget.destroy();
const res = await fetch('https://lancerouter.ai/api/token/', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'New-Api-User': String(result.data.id)
},
body: JSON.stringify({
name: 'my-token',
remain_quota: 1000000,
unlimited_quota: false
})
});
const data = await res.json();
```

---

## 同一页面多个实例

支持在同一页面创建多个 Widget 实例(如同时显示 Light 和 Dark 主题):
### 完整示例

```html
<div id="login-light"></div>
<div id="login-dark"></div>

<script src="https://your-domain.com/static/login-widget.js"></script>
<script>
var widget1 = NewApiLoginWidget.init({
container: '#login-light',
apiUrl: 'https://your-domain.com',
theme: 'light'
});

var widget2 = NewApiLoginWidget.init({
container: '#login-dark',
apiUrl: 'https://your-domain.com',
theme: 'dark'
});
</script>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>我的网站</title>
</head>
<body>
<form id="login-form">
<input id="username" placeholder="用户名或邮箱" required>
<input id="password" type="password" placeholder="密码" required>
<button type="submit">登录</button>
</form>
<p id="msg"></p>

<script src="https://lancerouter.ai/static/login-widget.js"></script>
<script>
document.getElementById('login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const result = await NewApiLoginWidget.login({
apiUrl: 'https://lancerouter.ai',
username: document.getElementById('username').value,
password: document.getElementById('password').value,
});
if (result.success) {
localStorage.setItem('user', JSON.stringify(result.data));
document.getElementById('msg').textContent = '欢迎, ' + result.data.username;
} else {
document.getElementById('msg').textContent = result.message;
}
});
</script>
</body>
</html>
```

---

## 注意事项

1. **跨域支持**:Widget 支持跨域嵌入。你的 New API 服务已配置 CORS,允许任意域名调用登录接口。
2. **HTTPS**:生产环境建议 New API 服务使用 HTTPS,否则部分浏览器功能可能受限。
3. **不管理会话**:Widget 只负责登录验证并返回用户数据,不会自动管理嵌入方的登录状态。你需要在 `onSuccess` 回调中自行处理用户会话。
4. **两步验证(2FA)**:当前版本不支持 2FA。如果用户启用了两步验证,`onError` 会收到提示信息。
5. **文件大小**:Widget JS 约 146KB(含 React 运行时),gzip 后约 48KB。

---

## 常见问题

### Q: 引入脚本后页面报错 `NewApiLoginWidget is not defined`

确保 `<script src="...login-widget.js">` 在调用 `init()` 的 `<script>` 之前。浏览器按顺序执行脚本,Widget 脚本必须先加载完成。

### Q: 登录成功后如何调用 New API 的其他接口

`onSuccess` 返回的数据中不包含 Token。你需要:

- 方案一:调用 `/api/token` 接口用用户密码创建一个 API Token
- 方案二:在 New API 后端登录后,使用返回的用户信息自行实现会话管理

### Q: 可以自定义样式吗

Widget 使用 Shadow DOM 隔离样式,外部 CSS 不会影响内部。当前仅支持 `light` 和 `dark` 两种内置主题。如需深度自定义,需要修改 `web/widget/styles.css` 并重新构建。

### Q: 如何重新构建 Widget

```bash
cd web/widget && bun install && bun run build
```

构建产物输出到 `web/dist/static/login-widget.js`。重新编译后端(`go build`)后生效。
1. **页面必须 HTTPS** — session cookie 带 `Secure` 标志,HTTP 页面下浏览器不会存储
2. **每个请求带 `New-Api-User` 头** — 值为登录返回的 `data.id`,否则接口返回未授权
3. **每个请求带 `credentials: 'include'`** — 否则浏览器不会携带 cookie
4. **不支持 2FA** — 启用两步验证的账户会返回失败提示
5. **Cookie 有效期 30 天**

+ 7
- 2
main.go Vedi File

@@ -183,8 +183,13 @@ func main() {
Path: "/",
MaxAge: 2592000, // 30 days
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteStrictMode,
Secure: os.Getenv("SESSION_SECURE") != "false",
SameSite: func() http.SameSite {
if os.Getenv("SESSION_SAMESITE") == "strict" {
return http.SameSiteStrictMode
}
return http.SameSiteNoneMode
}(),
})
// 支持通过环境变量自定义 session 名称,用于多节点部署时避免 cookie 冲突
sessionName := os.Getenv("SESSION_NAME")


+ 1
- 40
web/dist/static/login-widget.js
File diff soppresso perché troppo grande
Vedi File


+ 0
- 83
web/widget/LoginForm.jsx Vedi File

@@ -1,83 +0,0 @@
import React, { useState } from 'react';

export default function LoginForm({ apiUrl, onSuccess, onError }) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');

const handleSubmit = async (e) => {
e.preventDefault();
setError('');

if (!username.trim() || !password.trim()) {
const msg = '请输入用户名和密码';
setError(msg);
onError?.({ message: msg });
return;
}

setLoading(true);
try {
const res = await fetch(`${apiUrl}/api/user/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: username.trim(), password }),
});
const data = await res.json();

if (data.success) {
if (data.data?.require_2fa) {
const msg = '此账户启用了两步验证,暂不支持通过 Widget 登录';
setError(msg);
onError?.({ message: msg });
return;
}
onSuccess?.(data.data);
} else {
const msg = data.message || '登录失败';
setError(msg);
onError?.({ message: msg });
}
} catch (err) {
const msg = '网络错误,请检查网络连接';
setError(msg);
onError?.({ message: msg });
} finally {
setLoading(false);
}
};

return (
<form className="naw-form" onSubmit={handleSubmit}>
<div className="naw-field">
<label className="naw-label">用户名或邮箱</label>
<input
className="naw-input"
type="text"
placeholder="请输入用户名或邮箱地址"
value={username}
onChange={(e) => setUsername(e.target.value)}
autoComplete="username"
disabled={loading}
/>
</div>
<div className="naw-field">
<label className="naw-label">密码</label>
<input
className="naw-input"
type="password"
placeholder="请输入密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password"
disabled={loading}
/>
</div>
<p className="naw-error-msg">{error}</p>
<button className="naw-button" type="submit" disabled={loading}>
{loading ? '登录中...' : '登录'}
</button>
</form>
);
}

+ 0
- 109
web/widget/bun.lock Vedi File

@@ -4,55 +4,12 @@
"workspaces": {
"": {
"name": "new-api-login-widget",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.2.0",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.0", "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],

"@babel/compat-data": ["@babel/compat-data@7.29.3", "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.3.tgz", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],

"@babel/core": ["@babel/core@7.29.0", "https://registry.npmmirror.com/@babel/core/-/core-7.29.0.tgz", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],

"@babel/generator": ["@babel/generator@7.29.1", "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.1.tgz", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],

"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],

"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],

"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],

"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],

"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],

"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],

"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],

"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],

"@babel/helpers": ["@babel/helpers@7.29.2", "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.2.tgz", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],

"@babel/parser": ["@babel/parser@7.29.3", "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.3.tgz", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],

"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],

"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],

"@babel/template": ["@babel/template@7.28.6", "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],

"@babel/traverse": ["@babel/traverse@7.29.0", "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.0.tgz", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],

"@babel/types": ["@babel/types@7.29.0", "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],

"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],

"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
@@ -99,18 +56,6 @@

"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],

"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],

"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],

"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],

"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],

"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],

"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],

"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="],

"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="],
@@ -161,76 +106,22 @@

"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="],

"@types/babel__core": ["@types/babel__core@7.20.5", "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],

"@types/babel__generator": ["@types/babel__generator@7.27.0", "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],

"@types/babel__template": ["@types/babel__template@7.4.4", "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],

"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],

"@types/estree": ["@types/estree@1.0.8", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],

"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],

"baseline-browser-mapping": ["baseline-browser-mapping@2.10.31", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q=="],

"browserslist": ["browserslist@4.28.2", "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],

"caniuse-lite": ["caniuse-lite@1.0.30001793", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="],

"convert-source-map": ["convert-source-map@2.0.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],

"debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],

"electron-to-chromium": ["electron-to-chromium@1.5.360", "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.360.tgz", {}, "sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA=="],

"esbuild": ["esbuild@0.21.5", "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],

"escalade": ["escalade@3.2.0", "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],

"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],

"gensync": ["gensync@1.0.0-beta.2", "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],

"js-tokens": ["js-tokens@4.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],

"jsesc": ["jsesc@3.1.0", "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],

"json5": ["json5@2.2.3", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],

"loose-envify": ["loose-envify@1.4.0", "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],

"lru-cache": ["lru-cache@5.1.1", "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],

"ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],

"nanoid": ["nanoid@3.3.12", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],

"node-releases": ["node-releases@2.0.44", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.44.tgz", {}, "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ=="],

"picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],

"postcss": ["postcss@8.5.15", "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],

"react": ["react@18.3.1", "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],

"react-dom": ["react-dom@18.3.1", "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],

"react-refresh": ["react-refresh@0.17.0", "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],

"rollup": ["rollup@4.60.4", "https://registry.npmmirror.com/rollup/-/rollup-4.60.4.tgz", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="],

"scheduler": ["scheduler@0.23.2", "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],

"semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],

"source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],

"update-browserslist-db": ["update-browserslist-db@1.2.3", "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],

"vite": ["vite@5.4.21", "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],

"yallist": ["yallist@3.1.1", "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
}
}

+ 53
- 0
web/widget/index.js Vedi File

@@ -0,0 +1,53 @@
/**
* NewApiLoginWidget - Headless login SDK
*
* Usage:
* const result = await NewApiLoginWidget.login({
* apiUrl: 'https://your-domain.com',
* username: 'user',
* password: 'pass'
* });
*/

async function login({ apiUrl, username, password }) {
if (!apiUrl) {
throw new Error('NewApiLoginWidget: apiUrl is required');
}
if (!username || !username.trim()) {
return { success: false, message: '请输入用户名' };
}
if (!password) {
return { success: false, message: '请输入密码' };
}

const url = apiUrl.replace(/\/+$/, '') + '/api/user/login';

try {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ username: username.trim(), password }),
});

const data = await res.json();

if (data.success) {
if (data.data && data.data.require_2fa) {
return {
success: false,
message: '此账户启用了两步验证,暂不支持通过 Widget 登录',
};
}
return { success: true, data: data.data };
}

return { success: false, message: data.message || '登录失败' };
} catch (err) {
return { success: false, message: '网络错误,请检查网络连接' };
}
}

window.NewApiLoginWidget = { login };

export { login };

+ 0
- 71
web/widget/index.jsx Vedi File

@@ -1,71 +0,0 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import LoginForm from './LoginForm';
import cssText from './styles.css?inline';

function mount(container, props) {
const host = document.createElement('div');
host.setAttribute('data-new-api-widget', 'login');
container.appendChild(host);

const shadow = host.attachShadow({ mode: 'open' });

const style = document.createElement('style');
style.textContent = cssText;
shadow.appendChild(style);

if (props.theme === 'dark') {
host.setAttribute('theme', 'dark');
}

const mountPoint = document.createElement('div');
shadow.appendChild(mountPoint);

const root = createRoot(mountPoint);
root.render(
React.createElement(
'div',
{ className: 'naw-container' },
React.createElement('h1', { className: 'naw-title' }, '登 录'),
React.createElement(LoginForm, {
apiUrl: props.apiUrl,
onSuccess: props.onSuccess,
onError: props.onError,
})
)
);

return { host, root };
}

function init(options) {
const container =
typeof options.container === 'string'
? document.querySelector(options.container)
: options.container;

if (!container) {
throw new Error('NewApiLoginWidget: container not found');
}
if (!options.apiUrl) {
throw new Error('NewApiLoginWidget: apiUrl is required');
}

const { host, root } = mount(container, {
apiUrl: options.apiUrl.replace(/\/+$/, ''),
onSuccess: options.onSuccess,
onError: options.onError,
theme: options.theme || 'light',
});

return {
destroy() {
root.unmount();
host.remove();
},
};
}

window.NewApiLoginWidget = { init: init };

export { init };

+ 1
- 7
web/widget/package.json Vedi File

@@ -1,18 +1,12 @@
{
"name": "new-api-login-widget",
"version": "1.0.0",
"version": "2.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.2.0"
}
}

+ 0
- 115
web/widget/styles.css Vedi File

@@ -1,115 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');

:host {
--naw-primary: #4f46e5;
--naw-primary-hover: #4338ca;
--naw-bg: #ffffff;
--naw-text: #1f2937;
--naw-text-secondary: #6b7280;
--naw-border: #d1d5db;
--naw-input-bg: #f9fafb;
--naw-error: #ef4444;
--naw-radius: 8px;
--naw-font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;

display: block;
font-family: var(--naw-font);
}

:host([theme="dark"]) {
--naw-primary: #818cf8;
--naw-primary-hover: #6366f1;
--naw-bg: #1f2937;
--naw-text: #f9fafb;
--naw-text-secondary: #9ca3af;
--naw-border: #374151;
--naw-input-bg: #374151;
--naw-error: #f87171;
}

.naw-container {
max-width: 400px;
margin: 0 auto;
padding: 32px 24px;
background: var(--naw-bg);
border-radius: 16px;
}

.naw-title {
text-align: center;
font-size: 24px;
font-weight: 600;
color: var(--naw-text);
margin: 0 0 32px 0;
}

.naw-form {
display: flex;
flex-direction: column;
gap: 16px;
}

.naw-field {
display: flex;
flex-direction: column;
gap: 6px;
}

.naw-label {
font-size: 14px;
font-weight: 500;
color: var(--naw-text);
}

.naw-input {
width: 100%;
box-sizing: border-box;
padding: 10px 14px;
font-size: 15px;
border: 1px solid var(--naw-border);
border-radius: var(--naw-radius);
background: var(--naw-input-bg);
color: var(--naw-text);
outline: none;
transition: border-color 0.2s;
font-family: var(--naw-font);
}

.naw-input:focus {
border-color: var(--naw-primary);
}

.naw-input::placeholder {
color: var(--naw-text-secondary);
}

.naw-button {
width: 100%;
padding: 12px;
font-size: 15px;
font-weight: 600;
color: #ffffff;
background: var(--naw-primary);
border: none;
border-radius: var(--naw-radius);
cursor: pointer;
transition: background 0.2s;
font-family: var(--naw-font);
}

.naw-button:hover {
background: var(--naw-primary-hover);
}

.naw-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}

.naw-error-msg {
color: var(--naw-error);
font-size: 13px;
text-align: center;
margin: 0;
min-height: 20px;
}

+ 1
- 7
web/widget/vite.config.js Vedi File

@@ -1,15 +1,10 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
plugins: [react()],
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
build: {
lib: {
entry: resolve(__dirname, 'index.jsx'),
entry: resolve(__dirname, 'index.js'),
name: 'NewApiLoginWidget',
formats: ['iife'],
fileName: () => 'login-widget.js',
@@ -17,7 +12,6 @@ export default defineConfig({
outDir: resolve(__dirname, '../dist/static'),
emptyOutDir: false,
rollupOptions: {
external: [],
output: {
globals: {},
},


Caricamento…
Annulla
Salva