mirror of
https://github.com/chaitin/MonkeyCode.git
synced 2026-02-02 06:43:23 +08:00
8
.github/workflows/frontend-ci-cd.yml
vendored
8
.github/workflows/frontend-ci-cd.yml
vendored
@@ -3,7 +3,7 @@ name: Frontend CI/CD
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+*"
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+*'
|
||||
paths:
|
||||
- 'ui/**'
|
||||
- '.github/workflows/frontend-ci-cd.yml'
|
||||
@@ -29,12 +29,12 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '20.19.0'
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
version: 10.12.1
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
@@ -130,4 +130,4 @@ jobs:
|
||||
${{ env.REGISTRY }}/frontend:${{ needs.build.outputs.version }}
|
||||
${{ env.REGISTRY }}/frontend:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
21
ui/Makefile
Normal file
21
ui/Makefile
Normal file
@@ -0,0 +1,21 @@
|
||||
PLATFORM=linux/amd64
|
||||
TAG=main
|
||||
REGISTRY=monkeycode
|
||||
|
||||
|
||||
# 构建前端代码
|
||||
build:
|
||||
pnpm run build
|
||||
|
||||
# 构建并加载到本地Docker
|
||||
image: build
|
||||
docker buildx build \
|
||||
-f .Dockerfile \
|
||||
--platform ${PLATFORM} \
|
||||
--tag ${REGISTRY}/frontend:${TAG} \
|
||||
--load \
|
||||
.
|
||||
|
||||
save: image
|
||||
docker save -o /tmp/monkeycode_frontend.tar monkeycode/frontend:main
|
||||
|
||||
232
ui/src/api/Admin.ts
Normal file
232
ui/src/api/Admin.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||
* ## ##
|
||||
* ## AUTHOR: acacode ##
|
||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import request, { ContentType, RequestParams } from "./httpClient";
|
||||
import {
|
||||
DeleteDeleteAdminParams,
|
||||
DomainAdminUser,
|
||||
DomainCreateAdminReq,
|
||||
DomainListAdminLoginHistoryResp,
|
||||
DomainListAdminUserResp,
|
||||
DomainLoginReq,
|
||||
DomainSetting,
|
||||
DomainUpdateSettingReq,
|
||||
GetAdminLoginHistoryParams,
|
||||
GetListAdminUserParams,
|
||||
WebResp,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* @description 创建管理员
|
||||
*
|
||||
* @tags Admin
|
||||
* @name PostCreateAdmin
|
||||
* @summary 创建管理员
|
||||
* @request POST:/api/v1/admin/create
|
||||
* @response `200` `(WebResp & {
|
||||
data?: DomainAdminUser,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postCreateAdmin = (
|
||||
param: DomainCreateAdminReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
request<
|
||||
WebResp & {
|
||||
data?: DomainAdminUser;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/admin/create`,
|
||||
method: "POST",
|
||||
body: param,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 删除管理员
|
||||
*
|
||||
* @tags Admin
|
||||
* @name DeleteDeleteAdmin
|
||||
* @summary 删除管理员
|
||||
* @request DELETE:/api/v1/admin/delete
|
||||
* @response `200` `(WebResp & {
|
||||
data?: Record<string, any>,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const deleteDeleteAdmin = (
|
||||
query: DeleteDeleteAdminParams,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
request<
|
||||
WebResp & {
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/admin/delete`,
|
||||
method: "DELETE",
|
||||
query: query,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 获取管理员用户列表
|
||||
*
|
||||
* @tags Admin
|
||||
* @name GetListAdminUser
|
||||
* @summary 获取管理员用户列表
|
||||
* @request GET:/api/v1/admin/list
|
||||
* @response `200` `(WebResp & {
|
||||
data?: DomainListAdminUserResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const getListAdminUser = (
|
||||
query: GetListAdminUserParams,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
request<
|
||||
WebResp & {
|
||||
data?: DomainListAdminUserResp;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/admin/list`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 管理员登录
|
||||
*
|
||||
* @tags Admin
|
||||
* @name PostAdminLogin
|
||||
* @summary 管理员登录
|
||||
* @request POST:/api/v1/admin/login
|
||||
* @response `200` `(WebResp & {
|
||||
data?: DomainAdminUser,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postAdminLogin = (
|
||||
param: DomainLoginReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
request<
|
||||
WebResp & {
|
||||
data?: DomainAdminUser;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/admin/login`,
|
||||
method: "POST",
|
||||
body: param,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 获取管理员登录历史
|
||||
*
|
||||
* @tags Admin
|
||||
* @name GetAdminLoginHistory
|
||||
* @summary 获取管理员登录历史
|
||||
* @request GET:/api/v1/admin/login-history
|
||||
* @response `200` `(WebResp & {
|
||||
data?: DomainListAdminLoginHistoryResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const getAdminLoginHistory = (
|
||||
query: GetAdminLoginHistoryParams,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
request<
|
||||
WebResp & {
|
||||
data?: DomainListAdminLoginHistoryResp;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/admin/login-history`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 获取系统设置
|
||||
*
|
||||
* @tags Admin
|
||||
* @name GetGetSetting
|
||||
* @summary 获取系统设置
|
||||
* @request GET:/api/v1/admin/setting
|
||||
* @response `200` `(WebResp & {
|
||||
data?: DomainSetting,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const getGetSetting = (params: RequestParams = {}) =>
|
||||
request<
|
||||
WebResp & {
|
||||
data?: DomainSetting;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/admin/setting`,
|
||||
method: "GET",
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 更新系统设置
|
||||
*
|
||||
* @tags Admin
|
||||
* @name PutUpdateSetting
|
||||
* @summary 更新系统设置
|
||||
* @request PUT:/api/v1/admin/setting
|
||||
* @response `200` `(WebResp & {
|
||||
data?: DomainSetting,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const putUpdateSetting = (
|
||||
param: DomainUpdateSettingReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
request<
|
||||
WebResp & {
|
||||
data?: DomainSetting;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/admin/setting`,
|
||||
method: "PUT",
|
||||
body: param,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
@@ -12,25 +12,16 @@
|
||||
|
||||
import request, { ContentType, RequestParams } from "./httpClient";
|
||||
import {
|
||||
DeleteDeleteAdminParams,
|
||||
DeleteDeleteUserParams,
|
||||
DomainAdminUser,
|
||||
DomainCreateAdminReq,
|
||||
DomainInviteResp,
|
||||
DomainListAdminLoginHistoryResp,
|
||||
DomainListAdminUserResp,
|
||||
DomainListLoginHistoryResp,
|
||||
DomainListUserResp,
|
||||
DomainLoginReq,
|
||||
DomainLoginResp,
|
||||
DomainOAuthURLResp,
|
||||
DomainRegisterReq,
|
||||
DomainSetting,
|
||||
DomainUpdateSettingReq,
|
||||
DomainUpdateUserReq,
|
||||
DomainUser,
|
||||
GetAdminLoginHistoryParams,
|
||||
GetListAdminUserParams,
|
||||
GetListUserParams,
|
||||
GetLoginHistoryParams,
|
||||
GetUserOauthCallbackParams,
|
||||
@@ -38,212 +29,6 @@ import {
|
||||
WebResp,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* @description 创建管理员
|
||||
*
|
||||
* @tags User
|
||||
* @name PostCreateAdmin
|
||||
* @summary 创建管理员
|
||||
* @request POST:/api/v1/admin/create
|
||||
* @response `200` `(WebResp & {
|
||||
data?: DomainAdminUser,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postCreateAdmin = (
|
||||
param: DomainCreateAdminReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
request<
|
||||
WebResp & {
|
||||
data?: DomainAdminUser;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/admin/create`,
|
||||
method: "POST",
|
||||
body: param,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 删除管理员
|
||||
*
|
||||
* @tags User
|
||||
* @name DeleteDeleteAdmin
|
||||
* @summary 删除管理员
|
||||
* @request DELETE:/api/v1/admin/delete
|
||||
* @response `200` `(WebResp & {
|
||||
data?: Record<string, any>,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const deleteDeleteAdmin = (
|
||||
query: DeleteDeleteAdminParams,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
request<
|
||||
WebResp & {
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/admin/delete`,
|
||||
method: "DELETE",
|
||||
query: query,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 获取管理员用户列表
|
||||
*
|
||||
* @tags User
|
||||
* @name GetListAdminUser
|
||||
* @summary 获取管理员用户列表
|
||||
* @request GET:/api/v1/admin/list
|
||||
* @response `200` `(WebResp & {
|
||||
data?: DomainListAdminUserResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const getListAdminUser = (
|
||||
query: GetListAdminUserParams,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
request<
|
||||
WebResp & {
|
||||
data?: DomainListAdminUserResp;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/admin/list`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 管理员登录
|
||||
*
|
||||
* @tags User
|
||||
* @name PostAdminLogin
|
||||
* @summary 管理员登录
|
||||
* @request POST:/api/v1/admin/login
|
||||
* @response `200` `(WebResp & {
|
||||
data?: DomainAdminUser,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postAdminLogin = (
|
||||
param: DomainLoginReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
request<
|
||||
WebResp & {
|
||||
data?: DomainAdminUser;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/admin/login`,
|
||||
method: "POST",
|
||||
body: param,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 获取管理员登录历史
|
||||
*
|
||||
* @tags User
|
||||
* @name GetAdminLoginHistory
|
||||
* @summary 获取管理员登录历史
|
||||
* @request GET:/api/v1/admin/login-history
|
||||
* @response `200` `(WebResp & {
|
||||
data?: DomainListAdminLoginHistoryResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const getAdminLoginHistory = (
|
||||
query: GetAdminLoginHistoryParams,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
request<
|
||||
WebResp & {
|
||||
data?: DomainListAdminLoginHistoryResp;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/admin/login-history`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 获取系统设置
|
||||
*
|
||||
* @tags User
|
||||
* @name GetGetSetting
|
||||
* @summary 获取系统设置
|
||||
* @request GET:/api/v1/admin/setting
|
||||
* @response `200` `(WebResp & {
|
||||
data?: DomainSetting,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const getGetSetting = (params: RequestParams = {}) =>
|
||||
request<
|
||||
WebResp & {
|
||||
data?: DomainSetting;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/admin/setting`,
|
||||
method: "GET",
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 更新系统设置
|
||||
*
|
||||
* @tags User
|
||||
* @name PutUpdateSetting
|
||||
* @summary 更新系统设置
|
||||
* @request PUT:/api/v1/admin/setting
|
||||
* @response `200` `(WebResp & {
|
||||
data?: DomainSetting,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const putUpdateSetting = (
|
||||
param: DomainUpdateSettingReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
request<
|
||||
WebResp & {
|
||||
data?: DomainSetting;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/admin/setting`,
|
||||
method: "PUT",
|
||||
body: param,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 下载VSCode插件
|
||||
*
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './Admin'
|
||||
export * from './Billing'
|
||||
export * from './Dashboard'
|
||||
export * from './Model'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
/*
|
||||
@@ -10,37 +11,38 @@
|
||||
*/
|
||||
|
||||
export enum ConstsUserStatus {
|
||||
UserStatusActive = 'active',
|
||||
UserStatusInactive = 'inactive',
|
||||
UserStatusLocked = 'locked',
|
||||
UserStatusActive = "active",
|
||||
UserStatusInactive = "inactive",
|
||||
UserStatusLocked = "locked",
|
||||
}
|
||||
|
||||
export enum ConstsUserPlatform {
|
||||
UserPlatformEmail = 'email',
|
||||
UserPlatformDingTalk = 'dingtalk',
|
||||
UserPlatformEmail = "email",
|
||||
UserPlatformDingTalk = "dingtalk",
|
||||
UserPlatformCustom = "custom",
|
||||
}
|
||||
|
||||
export enum ConstsModelType {
|
||||
ModelTypeLLM = 'llm',
|
||||
ModelTypeCoder = 'coder',
|
||||
ModelTypeEmbedding = 'embedding',
|
||||
ModelTypeAudio = 'audio',
|
||||
ModelTypeReranker = 'reranker',
|
||||
ModelTypeLLM = "llm",
|
||||
ModelTypeCoder = "coder",
|
||||
ModelTypeEmbedding = "embedding",
|
||||
ModelTypeAudio = "audio",
|
||||
ModelTypeReranker = "reranker",
|
||||
}
|
||||
|
||||
export enum ConstsModelStatus {
|
||||
ModelStatusActive = 'active',
|
||||
ModelStatusInactive = 'inactive',
|
||||
ModelStatusActive = "active",
|
||||
ModelStatusInactive = "inactive",
|
||||
}
|
||||
|
||||
export enum ConstsChatRole {
|
||||
ChatRoleUser = 'user',
|
||||
ChatRoleAssistant = 'assistant',
|
||||
ChatRoleUser = "user",
|
||||
ChatRoleAssistant = "assistant",
|
||||
}
|
||||
|
||||
export enum ConstsAdminStatus {
|
||||
AdminStatusActive = 'active',
|
||||
AdminStatusInactive = 'inactive',
|
||||
AdminStatusActive = "active",
|
||||
AdminStatusInactive = "inactive",
|
||||
}
|
||||
|
||||
export interface DomainAcceptCompletionReq {
|
||||
@@ -183,6 +185,40 @@ export interface DomainCreateModelReq {
|
||||
provider?: string;
|
||||
}
|
||||
|
||||
export interface DomainCustomOAuth {
|
||||
/** 自定义OAuth访问令牌URL */
|
||||
access_token_url?: string;
|
||||
/** 自定义OAuth授权URL */
|
||||
authorize_url?: string;
|
||||
/** 用户信息回包中的头像URL字段名` */
|
||||
avatar_field?: string;
|
||||
/** 自定义客户端ID */
|
||||
client_id?: string;
|
||||
/** 自定义客户端密钥 */
|
||||
client_secret?: string;
|
||||
/** 用户信息回包中的邮箱字段名 */
|
||||
email_field?: string;
|
||||
/** 自定义OAuth开关 */
|
||||
enable?: boolean;
|
||||
/** 用户信息回包中的ID字段名 */
|
||||
id_field?: string;
|
||||
/** 用户信息回包中的用户名字段名` */
|
||||
name_field?: string;
|
||||
/** 自定义OAuth Scope列表 */
|
||||
scopes?: string[];
|
||||
/** 自定义OAuth用户信息URL */
|
||||
userinfo_url?: string;
|
||||
}
|
||||
|
||||
export interface DomainDingtalkOAuth {
|
||||
/** 钉钉客户端ID */
|
||||
client_id?: string;
|
||||
/** 钉钉客户端密钥 */
|
||||
client_secret?: string;
|
||||
/** 钉钉OAuth开关 */
|
||||
enable?: boolean;
|
||||
}
|
||||
|
||||
export interface DomainIPInfo {
|
||||
/** ASN */
|
||||
asn?: string;
|
||||
@@ -355,10 +391,12 @@ export interface DomainRegisterReq {
|
||||
export interface DomainSetting {
|
||||
/** 创建时间 */
|
||||
created_at?: number;
|
||||
/** 自定义OAuth接入 */
|
||||
custom_oauth?: DomainCustomOAuth;
|
||||
/** 钉钉OAuth接入 */
|
||||
dingtalk_oauth?: DomainDingtalkOAuth;
|
||||
/** 是否禁用密码登录 */
|
||||
disable_password_login?: boolean;
|
||||
/** 是否开启钉钉OAuth */
|
||||
enable_dingtalk_oauth?: boolean;
|
||||
/** 是否开启SSO */
|
||||
enable_sso?: boolean;
|
||||
/** 是否强制两步验证 */
|
||||
@@ -445,14 +483,12 @@ export interface DomainUpdateModelReq {
|
||||
}
|
||||
|
||||
export interface DomainUpdateSettingReq {
|
||||
/** 钉钉客户端ID */
|
||||
dingtalk_client_id?: string;
|
||||
/** 钉钉客户端密钥 */
|
||||
dingtalk_client_secret?: string;
|
||||
/** 自定义OAuth配置 */
|
||||
custom_oauth?: DomainCustomOAuth;
|
||||
/** 钉钉OAuth配置 */
|
||||
dingtalk_oauth?: DomainDingtalkOAuth;
|
||||
/** 是否禁用密码登录 */
|
||||
disable_password_login?: boolean;
|
||||
/** 是否开启钉钉OAuth */
|
||||
enable_dingtalk_oauth?: boolean;
|
||||
/** 是否开启SSO */
|
||||
enable_sso?: boolean;
|
||||
/** 是否强制两步验证 */
|
||||
@@ -469,6 +505,8 @@ export interface DomainUpdateUserReq {
|
||||
}
|
||||
|
||||
export interface DomainUser {
|
||||
/** 头像URL */
|
||||
avatar_url?: string;
|
||||
/** 创建时间 */
|
||||
created_at?: number;
|
||||
/** 邮箱 */
|
||||
@@ -639,11 +677,15 @@ export interface GetCategoryStatDashboardParams {
|
||||
* 持续时间 (小时或天数)`
|
||||
* @min 24
|
||||
* @max 90
|
||||
* @default 90
|
||||
*/
|
||||
duration?: number;
|
||||
/** 精度: "hour", "day" */
|
||||
precision: 'hour' | 'day';
|
||||
/** 用户ID,可jj */
|
||||
/**
|
||||
* 精度: "hour", "day"
|
||||
* @default "day"
|
||||
*/
|
||||
precision: "hour" | "day";
|
||||
/** 用户ID,可选参数 */
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
@@ -652,11 +694,15 @@ export interface GetTimeStatDashboardParams {
|
||||
* 持续时间 (小时或天数)`
|
||||
* @min 24
|
||||
* @max 90
|
||||
* @default 90
|
||||
*/
|
||||
duration?: number;
|
||||
/** 精度: "hour", "day" */
|
||||
precision: 'hour' | 'day';
|
||||
/** 用户ID,可jj */
|
||||
/**
|
||||
* 精度: "hour", "day"
|
||||
* @default "day"
|
||||
*/
|
||||
precision: "hour" | "day";
|
||||
/** 用户ID,可选参数 */
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
@@ -665,11 +711,15 @@ export interface GetUserCodeRankDashboardParams {
|
||||
* 持续时间 (小时或天数)`
|
||||
* @min 24
|
||||
* @max 90
|
||||
* @default 90
|
||||
*/
|
||||
duration?: number;
|
||||
/** 精度: "hour", "day" */
|
||||
precision: 'hour' | 'day';
|
||||
/** 用户ID,可jj */
|
||||
/**
|
||||
* 精度: "hour", "day"
|
||||
* @default "day"
|
||||
*/
|
||||
precision: "hour" | "day";
|
||||
/** 用户ID,可选参数 */
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
@@ -678,8 +728,15 @@ export interface GetUserEventsDashboardParams {
|
||||
* 持续时间 (小时或天数)`
|
||||
* @min 24
|
||||
* @max 90
|
||||
* @default 90
|
||||
*/
|
||||
/** 用户ID,可jj */
|
||||
duration?: number;
|
||||
/**
|
||||
* 精度: "hour", "day"
|
||||
* @default "day"
|
||||
*/
|
||||
precision: "hour" | "day";
|
||||
/** 用户ID,可选参数 */
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
@@ -693,22 +750,26 @@ export interface GetUserStatDashboardParams {
|
||||
* 持续时间 (小时或天数)`
|
||||
* @min 24
|
||||
* @max 90
|
||||
* @default 90
|
||||
*/
|
||||
duration?: number;
|
||||
/** 精度: "hour", "day" */
|
||||
precision: 'hour' | 'day';
|
||||
/** 用户ID,可jj */
|
||||
/**
|
||||
* 精度: "hour", "day"
|
||||
* @default "day"
|
||||
*/
|
||||
precision: "hour" | "day";
|
||||
/** 用户ID,可选参数 */
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
export interface GetMyModelListParams {
|
||||
/** 模型类型 llm:对话模型 coder:代码模型 */
|
||||
model_type?: 'llm' | 'coder' | 'embedding' | 'audio' | 'reranker';
|
||||
model_type?: "llm" | "coder" | "embedding" | "audio" | "reranker";
|
||||
}
|
||||
|
||||
export interface GetGetTokenUsageParams {
|
||||
/** 模型类型 llm:对话模型 coder:代码模型 */
|
||||
model_type: 'llm' | 'coder' | 'embedding' | 'audio' | 'reranker';
|
||||
model_type: "llm" | "coder" | "embedding" | "audio" | "reranker";
|
||||
}
|
||||
|
||||
export interface DeleteDeleteUserParams {
|
||||
@@ -740,8 +801,10 @@ export interface GetUserOauthCallbackParams {
|
||||
}
|
||||
|
||||
export interface GetUserOauthSignupOrInParams {
|
||||
/** 邀请码 */
|
||||
inviate_code?: string;
|
||||
/** 第三方平台 dingtalk */
|
||||
platform: 'email' | 'dingtalk';
|
||||
platform: "email" | "dingtalk" | "custom";
|
||||
/** 登录成功后跳转的 URL */
|
||||
redirect_url?: string;
|
||||
/** 会话ID */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styled, FormLabel } from '@mui/material';
|
||||
import { styled, FormLabel, Box } from '@mui/material';
|
||||
|
||||
export const StyledFormLabel = styled(FormLabel)(({ theme }) => ({
|
||||
display: 'block',
|
||||
@@ -10,3 +10,20 @@ export const StyledFormLabel = styled(FormLabel)(({ theme }) => ({
|
||||
fontSize: 14,
|
||||
},
|
||||
}));
|
||||
|
||||
export const FormItem = ({
|
||||
label,
|
||||
children,
|
||||
required,
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
required?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Box>
|
||||
<StyledFormLabel required={required}>{label}</StyledFormLabel>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,111 +1,121 @@
|
||||
import KeyboardArrowRightRoundedIcon from '@mui/icons-material/KeyboardArrowRightRounded';
|
||||
import { Box, Stack, useTheme } from '@mui/material';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, Stack, Typography } from '@mui/material';
|
||||
import { useMemo } from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
|
||||
const HomeBread = { title: '工作台', to: '/' };
|
||||
const OtherBread = {
|
||||
const ADMIN_BREADCRUMB_MAP: Record<string, { title: string; to: string }> = {
|
||||
dashboard: { title: '仪表盘', to: '/' },
|
||||
chat: { title: '对话记录', to: '/chat' },
|
||||
completion: { title: '补全记录', to: '/completion' },
|
||||
model: { title: '模型管理', to: '/model' },
|
||||
user: { title: '成员管理', to: '/user' },
|
||||
'user-management': { title: '成员管理', to: '/user-management' },
|
||||
admin: { title: '管理员', to: '/admin' },
|
||||
};
|
||||
|
||||
const Bread = () => {
|
||||
const theme = useTheme();
|
||||
const { pathname } = useLocation();
|
||||
const [breads, setBreads] = useState<
|
||||
{ title: React.ReactNode; to: string }[]
|
||||
>([]);
|
||||
const USER_BREADCRUMB_MAP: Record<string, { title: string; to: string }> = {
|
||||
dashboard: { title: '仪表盘', to: '/user/dashboard' },
|
||||
chat: { title: '对话记录', to: '/user/chat' },
|
||||
completion: { title: '补全记录', to: '/user/completion' },
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const curBreads: { title: React.ReactNode; to: string }[] = [
|
||||
{
|
||||
title: (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
}}
|
||||
>
|
||||
MonkeyCode
|
||||
</Box>
|
||||
),
|
||||
to: '/dashboard',
|
||||
},
|
||||
];
|
||||
if (pathname === '/') {
|
||||
curBreads.push(HomeBread);
|
||||
} else {
|
||||
const pieces = pathname.split('/').filter((it) => it !== '');
|
||||
pieces.forEach((it) => {
|
||||
const bread = OtherBread[it as keyof typeof OtherBread];
|
||||
if (bread) {
|
||||
curBreads.push(bread);
|
||||
const Bread = () => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const breadcrumbs = useMemo(() => {
|
||||
const pathParts = pathname.split('/').filter(Boolean);
|
||||
|
||||
const generatedCrumbs = pathParts
|
||||
.map((part) => {
|
||||
if (pathname.startsWith('/user/')) {
|
||||
return USER_BREADCRUMB_MAP[part];
|
||||
}
|
||||
});
|
||||
}
|
||||
// if (pageName) {
|
||||
// curBreads.push({ title: pageName, to: 'custom' })
|
||||
// }
|
||||
setBreads(curBreads);
|
||||
return ADMIN_BREADCRUMB_MAP[part];
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'MonkeyCode',
|
||||
to: pathname.startsWith('/user/') ? '/user/dashboard' : '/dashboard',
|
||||
},
|
||||
...generatedCrumbs,
|
||||
];
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
direction='row'
|
||||
alignItems='center'
|
||||
gap={1}
|
||||
component='nav'
|
||||
aria-label='breadcrumb'
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
// color: 'text.auxiliary',
|
||||
fontSize: '14px',
|
||||
// a: { color: 'text.auxiliary' },
|
||||
}}
|
||||
>
|
||||
{/* <KBSelect /> */}
|
||||
{breads.map((it, idx) => {
|
||||
return (
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={1}
|
||||
key={idx}
|
||||
sx={{
|
||||
color:
|
||||
idx === breads.length - 1
|
||||
? `${theme.palette.text.primary} !important`
|
||||
: 'text.disabled',
|
||||
{breadcrumbs.map((crumb, index) => {
|
||||
const isLast = index === breadcrumbs.length - 1;
|
||||
|
||||
...(idx === breads.length - 1 && { fontWeight: 'bold' }),
|
||||
}}
|
||||
>
|
||||
{idx !== 0 && (
|
||||
const crumbContent = (
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
{index > 0 && (
|
||||
<KeyboardArrowRightRoundedIcon sx={{ fontSize: 14 }} />
|
||||
)}
|
||||
{it.to === 'custom' ? (
|
||||
<Box
|
||||
sx={{ cursor: 'pointer', ':hover': { color: 'primary.main' } }}
|
||||
>
|
||||
{it.title}
|
||||
</Box>
|
||||
) : (
|
||||
<NavLink to={it.to}>
|
||||
<Box
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
':hover': { color: 'primary.main' },
|
||||
}}
|
||||
>
|
||||
{it.title}
|
||||
</Box>
|
||||
</NavLink>
|
||||
)}
|
||||
<Typography
|
||||
variant='body2'
|
||||
sx={{
|
||||
fontWeight: isLast ? 'bold' : 'normal',
|
||||
}}
|
||||
>
|
||||
{crumb.title}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
if (isLast) {
|
||||
return (
|
||||
<Box key={index} sx={{ color: 'text.primary' }}>
|
||||
{crumbContent}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (crumb.to === 'custom') {
|
||||
return (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
color: 'text.disabled',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
color: 'primary.main',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{crumbContent}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
key={index}
|
||||
to={crumb.to}
|
||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
color: 'text.disabled',
|
||||
'&:hover': {
|
||||
color: 'primary.main',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{crumbContent}
|
||||
</Box>
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -108,7 +108,7 @@ const Code = ({
|
||||
accessibilitySupport: 'off',
|
||||
bracketPairColorization: { enabled: false },
|
||||
matchBrackets: 'never',
|
||||
lineNumbers: 'on',
|
||||
lineNumbers: 'off',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontalScrollbarSize: 0,
|
||||
scrollbar: {
|
||||
|
||||
@@ -61,7 +61,7 @@ const Diff: React.FC<DiffProps> = ({
|
||||
fontSize: 14,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'off',
|
||||
lineNumbers: 'on',
|
||||
lineNumbers: 'off',
|
||||
glyphMargin: false,
|
||||
folding: false,
|
||||
overviewRulerLanes: 0,
|
||||
|
||||
@@ -2,13 +2,12 @@ import Logo from '@/assets/images/logo.png';
|
||||
import { alpha, Box, Button, Stack, useTheme, styled } from '@mui/material';
|
||||
import { Icon } from '@c-x/ui';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import Avatar from '../avatar';
|
||||
import { Modal } from '@c-x/ui';
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import Qrcode from '@/assets/images/qrcode.png';
|
||||
import Version from './version';
|
||||
|
||||
const menus = [
|
||||
const ADMIN_MENUS = [
|
||||
{
|
||||
label: '仪表盘',
|
||||
value: '/dashboard',
|
||||
@@ -46,8 +45,8 @@ const menus = [
|
||||
},
|
||||
{
|
||||
label: '成员管理',
|
||||
value: '/user',
|
||||
pathname: 'user',
|
||||
value: '/user-management',
|
||||
pathname: 'user-management',
|
||||
icon: 'icon-yonghuguanli1',
|
||||
show: true,
|
||||
},
|
||||
@@ -60,6 +59,30 @@ const menus = [
|
||||
},
|
||||
];
|
||||
|
||||
const USER_MENUS = [
|
||||
{
|
||||
label: '仪表盘',
|
||||
value: '/user/dashboard',
|
||||
pathname: '/user/dashboard',
|
||||
icon: 'icon-yibiaopan',
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
label: '对话记录',
|
||||
value: '/user/chat',
|
||||
pathname: '/user/chat',
|
||||
icon: 'icon-duihuajilu1',
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
label: '补全记录',
|
||||
value: '/user/completion',
|
||||
pathname: '/user/completion',
|
||||
icon: 'icon-buquanjilu',
|
||||
show: true,
|
||||
},
|
||||
];
|
||||
|
||||
const SidebarButton = styled(Button)(({ theme }) => ({
|
||||
fontSize: 14,
|
||||
flexShrink: 0,
|
||||
@@ -81,6 +104,13 @@ const Sidebar = () => {
|
||||
const { pathname } = useLocation();
|
||||
const theme = useTheme();
|
||||
const [showQrcode, setShowQrcode] = useState(false);
|
||||
const menus = useMemo(() => {
|
||||
if (pathname.startsWith('/user/')) {
|
||||
return USER_MENUS;
|
||||
}
|
||||
return ADMIN_MENUS;
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
TextField,
|
||||
Paper,
|
||||
} from '@mui/material';
|
||||
import { postCreateAdmin } from '@/api/User';
|
||||
import { postCreateAdmin } from '@/api/Admin';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { deleteDeleteAdmin, getListAdminUser } from '@/api/User';
|
||||
import { deleteDeleteAdmin, getListAdminUser } from '@/api/Admin';
|
||||
import { Table, Modal, message } from '@c-x/ui';
|
||||
import { ColumnsType } from '@c-x/ui/dist/Table';
|
||||
import { useRequest } from 'ahooks';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Stack, Box } from '@mui/material';
|
||||
import { Table } from '@c-x/ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { getAdminLoginHistory } from '@/api/User';
|
||||
import { getAdminLoginHistory } from '@/api/Admin';
|
||||
import { ColumnsType } from '@c-x/ui/dist/Table';
|
||||
import { DomainListAdminLoginHistoryResp } from '@/api/types';
|
||||
import User from '@/components/user';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import Logo from '@/assets/images/logo.png';
|
||||
import {
|
||||
Box,
|
||||
@@ -21,11 +21,13 @@ import { Icon, message } from '@c-x/ui';
|
||||
import { AestheticFluidBg } from '@/assets/jsm/AestheticFluidBg.module.js';
|
||||
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { postLogin, getUserOauthSignupOrIn, getGetSetting } from '@/api/User';
|
||||
import { postLogin, getUserOauthSignupOrIn } from '@/api/User';
|
||||
import { getGetSetting } from '@/api/Admin';
|
||||
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { DomainSetting } from '@/api/types';
|
||||
|
||||
// 样式化组件
|
||||
const StyledContainer = styled(Container)(({ theme }) => ({
|
||||
@@ -114,8 +116,9 @@ const AuthPage = () => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const { data: loginSetting = {} } = useRequest(getGetSetting);
|
||||
|
||||
const { data: loginSetting = {} as DomainSetting } =
|
||||
useRequest(getGetSetting);
|
||||
const { custom_oauth = {}, dingtalk_oauth = {} } = loginSetting;
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
@@ -174,6 +177,12 @@ const AuthPage = () => {
|
||||
new AestheticFluidBg(BACKGROUND_CONFIG);
|
||||
}, []);
|
||||
|
||||
const oauthEnable = useMemo(() => {
|
||||
return (
|
||||
loginSetting.custom_oauth?.enable || loginSetting.dingtalk_oauth?.enable
|
||||
);
|
||||
}, [loginSetting]);
|
||||
|
||||
// 渲染用户名输入框
|
||||
const renderUsernameField = () => (
|
||||
<Controller
|
||||
@@ -265,9 +274,9 @@ const AuthPage = () => {
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const onDingdingLogin = () => {
|
||||
const onOauthLogin = (platform: 'dingtalk' | 'custom') => {
|
||||
getUserOauthSignupOrIn({
|
||||
platform: 'dingtalk',
|
||||
platform,
|
||||
redirect_url: window.location.origin + window.location.pathname,
|
||||
// @ts-ignore
|
||||
session_id: searchParams.get('session_id') || null,
|
||||
@@ -278,15 +287,28 @@ const AuthPage = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const dingdingLogin = () => {
|
||||
const oauthLogin = () => {
|
||||
return (
|
||||
<Stack justifyContent='center'>
|
||||
<Divider sx={{ my: 3, fontSize: 12, borderColor: 'divider' }}>
|
||||
使用其他方式登录
|
||||
</Divider>
|
||||
<IconButton sx={{ alignSelf: 'center' }} onClick={onDingdingLogin}>
|
||||
<Icon type='icon-dingding' sx={{ fontSize: 30 }} />
|
||||
</IconButton>
|
||||
{dingtalk_oauth.enable && (
|
||||
<IconButton
|
||||
sx={{ alignSelf: 'center' }}
|
||||
onClick={() => onOauthLogin('dingtalk')}
|
||||
>
|
||||
<Icon type='icon-dingding' sx={{ fontSize: 30 }} />
|
||||
</IconButton>
|
||||
)}
|
||||
{custom_oauth.enable && (
|
||||
<IconButton
|
||||
sx={{ alignSelf: 'center' }}
|
||||
onClick={() => onOauthLogin('custom')}
|
||||
>
|
||||
<Icon type='icon-oauth' sx={{ fontSize: 30 }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -322,7 +344,7 @@ const AuthPage = () => {
|
||||
</LogoTitle>
|
||||
</LogoContainer>
|
||||
{!loginSetting.disable_password_login && renderLoginForm()}
|
||||
{loginSetting.enable_dingtalk_oauth && dingdingLogin()}
|
||||
{oauthEnable && oauthLogin()}
|
||||
</StyledPaper>
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
@@ -35,6 +35,7 @@ const Chat = () => {
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
// eslint-disable-next-line
|
||||
}, [page, size]);
|
||||
|
||||
const columns: ColumnsType<DomainChatRecord> = [
|
||||
@@ -102,7 +103,7 @@ const Chat = () => {
|
||||
};
|
||||
return (
|
||||
<StyledLabel color={value ? workModeMap[value]['color'] : 'default'}>
|
||||
{ value ? workModeMap[value]['name'] : '未知' }
|
||||
{value ? workModeMap[value]['name'] : '未知'}
|
||||
</StyledLabel>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -123,7 +123,7 @@ const ChatDetailModal = ({
|
||||
fontSize: 14,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
lineNumbers: 'on',
|
||||
lineNumbers: 'off',
|
||||
glyphMargin: false,
|
||||
folding: false,
|
||||
overviewRulerLanes: 0,
|
||||
|
||||
@@ -54,8 +54,8 @@ const MemberInfo = ({
|
||||
}: {
|
||||
data: DomainUserHeatmapResp;
|
||||
memberData: DomainUser | null;
|
||||
userList: DomainUser[];
|
||||
onMemberChange: (data: DomainUser) => void;
|
||||
userList?: DomainUser[];
|
||||
onMemberChange?: (data: DomainUser) => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [blockSize, setBlockSize] = useState(8);
|
||||
@@ -104,12 +104,12 @@ const MemberInfo = ({
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
{userList.map((item) => (
|
||||
{userList?.map((item) => (
|
||||
<MenuItem
|
||||
key={item.id}
|
||||
selected={memberData?.id === item.id}
|
||||
onClick={() => {
|
||||
onMemberChange(item);
|
||||
onMemberChange?.(item);
|
||||
handleClose();
|
||||
}}
|
||||
sx={{
|
||||
@@ -127,12 +127,14 @@ const MemberInfo = ({
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
<Avatar name={memberData?.username || ''} />
|
||||
<IconButton onClick={handleClick} size='small'>
|
||||
<Icon
|
||||
type='icon-qiehuan'
|
||||
sx={{ fontSize: 16, color: 'text.primary' }}
|
||||
/>
|
||||
</IconButton>
|
||||
{userList && (
|
||||
<IconButton onClick={handleClick} size='small'>
|
||||
<Icon
|
||||
type='icon-qiehuan'
|
||||
sx={{ fontSize: 16, color: 'text.primary' }}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
<Box sx={{ fontSize: 16, fontWeight: 700 }}>
|
||||
{memberData?.username}
|
||||
|
||||
@@ -42,6 +42,7 @@ const MemberStatistic = ({
|
||||
() =>
|
||||
getUserEventsDashboard({
|
||||
user_id: id || '',
|
||||
precision: timeDuration.precision,
|
||||
}),
|
||||
{
|
||||
refreshDeps: [id],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Logo from '@/assets/images/logo.png';
|
||||
|
||||
@@ -20,14 +20,13 @@ import {
|
||||
IconButton,
|
||||
CircularProgress,
|
||||
Stack,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import { useRequest } from 'ahooks';
|
||||
import {
|
||||
postRegister,
|
||||
getUserOauthSignupOrIn,
|
||||
getGetSetting,
|
||||
} from '@/api/User';
|
||||
import { postRegister, getUserOauthSignupOrIn } from '@/api/User';
|
||||
import { getGetSetting } from '@/api/Admin';
|
||||
import { Icon } from '@c-x/ui';
|
||||
import { DomainSetting } from '@/api/types';
|
||||
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import MenuBookIcon from '@mui/icons-material/MenuBook';
|
||||
@@ -91,7 +90,9 @@ const StyledTextField = styled(TextField)(({ theme }) => ({
|
||||
const Invite = () => {
|
||||
const { id, step } = useParams();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const { data: loginSetting = {} } = useRequest(getGetSetting);
|
||||
const { data: loginSetting = {} as DomainSetting } =
|
||||
useRequest(getGetSetting);
|
||||
const { custom_oauth = {}, dingtalk_oauth = {} } = loginSetting;
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
@@ -134,19 +135,54 @@ const Invite = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onDingdingLogin = () => {
|
||||
const onOauthLogin = (platform: 'dingtalk' | 'custom') => {
|
||||
getUserOauthSignupOrIn({
|
||||
platform: 'dingtalk',
|
||||
platform,
|
||||
redirect_url: `${window.location.origin}/invite/${id}/2`,
|
||||
inviate_code: id,
|
||||
}).then((res) => {
|
||||
window.location.href = res.url!;
|
||||
if (res.url) {
|
||||
window.location.href = res.url;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const oauthEnable = useMemo(() => {
|
||||
return (
|
||||
loginSetting.custom_oauth?.enable || loginSetting.dingtalk_oauth?.enable
|
||||
);
|
||||
}, [loginSetting]);
|
||||
|
||||
const oauthLogin = () => {
|
||||
return (
|
||||
<Stack justifyContent='center' gap={2}>
|
||||
<Divider sx={{ my: 2, fontSize: 12, borderColor: 'divider' }}>
|
||||
使用以下方式注册
|
||||
</Divider>
|
||||
{dingtalk_oauth.enable && (
|
||||
<Button
|
||||
sx={{ alignSelf: 'center' }}
|
||||
onClick={() => onOauthLogin('dingtalk')}
|
||||
>
|
||||
<Icon type='icon-dingding' sx={{ fontSize: 30 }} />
|
||||
</Button>
|
||||
)}
|
||||
{custom_oauth.enable && (
|
||||
<IconButton
|
||||
sx={{ alignSelf: 'center' }}
|
||||
onClick={() => onOauthLogin('custom')}
|
||||
>
|
||||
<Icon type='icon-oauth' sx={{ fontSize: 30 }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (activeStep) {
|
||||
case 1:
|
||||
return !loginSetting.enable_dingtalk_oauth ? (
|
||||
return !oauthEnable ? (
|
||||
<Box component='form' onSubmit={onRegister}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={12}>
|
||||
@@ -285,17 +321,7 @@ const Invite = () => {
|
||||
</Grid>
|
||||
</Box>
|
||||
) : (
|
||||
<Stack>
|
||||
<Button
|
||||
size='large'
|
||||
variant='contained'
|
||||
sx={{ alignSelf: 'center' }}
|
||||
onClick={onDingdingLogin}
|
||||
>
|
||||
<Icon type='icon-dingding' sx={{ fontSize: 20, mr: 1 }} />
|
||||
使用钉钉登录
|
||||
</Button>
|
||||
</Stack>
|
||||
oauthLogin()
|
||||
);
|
||||
|
||||
case 2:
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
InputAdornment,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import { postAdminLogin } from '@/api/User';
|
||||
import { postAdminLogin } from '@/api/Admin';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import Card from '@/components/card';
|
||||
import {
|
||||
Grid2 as Grid,
|
||||
@@ -8,9 +8,8 @@ import {
|
||||
Button,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import { Icon, Modal } from '@c-x/ui';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { getGetSetting, putUpdateSetting } from '@/api/User';
|
||||
import { getGetSetting, putUpdateSetting } from '@/api/Admin';
|
||||
import MemberManage from './memberManage';
|
||||
import LoginHistory from './loginHistory';
|
||||
import { message } from '@c-x/ui';
|
||||
@@ -30,18 +29,19 @@ const StyledLabel = styled('div')(({ theme }) => ({
|
||||
color: theme.vars.palette.text.primary,
|
||||
}));
|
||||
|
||||
const OAUTH_LOGIN_TYPE_KEYS = ['dingtalk_oauth', 'custom_oauth'];
|
||||
|
||||
const OAUTH_LOGIN_TYPE_LABELS = {
|
||||
custom_oauth: '已开启 OAuth 登录',
|
||||
dingtalk_oauth: '已开启钉钉登录',
|
||||
};
|
||||
|
||||
type OAUTH_LOGIN_TYPE_KEYS = keyof typeof OAUTH_LOGIN_TYPE_LABELS;
|
||||
|
||||
const User = () => {
|
||||
const [thirdPartyLoginSettingModalOpen, setThirdPartyLoginSettingModalOpen] =
|
||||
useState(false);
|
||||
const {
|
||||
data = {
|
||||
enable_sso: false,
|
||||
force_two_factor_auth: false,
|
||||
disable_password_login: false,
|
||||
enable_dingtalk_oauth: false,
|
||||
},
|
||||
refresh,
|
||||
} = useRequest(getGetSetting);
|
||||
const { data, refresh } = useRequest(getGetSetting);
|
||||
|
||||
const { runAsync: updateSetting } = useRequest(putUpdateSetting, {
|
||||
manual: true,
|
||||
@@ -51,6 +51,16 @@ const User = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const oauthLabel = useMemo(() => {
|
||||
if (!data) return '未开启';
|
||||
const key = OAUTH_LOGIN_TYPE_KEYS.find(
|
||||
(key) => data[key as OAUTH_LOGIN_TYPE_KEYS]?.enable
|
||||
);
|
||||
return key
|
||||
? OAUTH_LOGIN_TYPE_LABELS[key as OAUTH_LOGIN_TYPE_KEYS]
|
||||
: '未开启';
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Stack gap={2} sx={{ height: '100%' }}>
|
||||
<Grid container spacing={2} sx={{ height: '100%' }}>
|
||||
@@ -84,12 +94,12 @@ const User = () => {
|
||||
component='span'
|
||||
sx={{
|
||||
ml: 2,
|
||||
color: data.enable_dingtalk_oauth ? 'success.main' : 'gray',
|
||||
color: oauthLabel ? 'success.main' : 'gray',
|
||||
fontWeight: 400,
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
{data.enable_dingtalk_oauth ? '已开启钉钉登录' : '未开启'}
|
||||
{oauthLabel}
|
||||
</Box>
|
||||
</StyledLabel>
|
||||
<Button
|
||||
@@ -108,7 +118,7 @@ const User = () => {
|
||||
<ThirdPartyLoginSettingModal
|
||||
open={thirdPartyLoginSettingModalOpen}
|
||||
onCancel={() => setThirdPartyLoginSettingModalOpen(false)}
|
||||
settingData={data}
|
||||
settingData={data || {}}
|
||||
onOk={() => {
|
||||
refresh();
|
||||
}}
|
||||
514
ui/src/pages/user-management/thirdPartyLoginSettingModal.tsx
Normal file
514
ui/src/pages/user-management/thirdPartyLoginSettingModal.tsx
Normal file
@@ -0,0 +1,514 @@
|
||||
import {
|
||||
Button,
|
||||
Radio,
|
||||
Stack,
|
||||
TextField,
|
||||
Autocomplete,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import { Modal, message } from '@c-x/ui';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { FormItem } from '@/components/form';
|
||||
import { putUpdateSetting } from '@/api/Admin';
|
||||
import { DomainSetting, DomainUpdateSettingReq } from '@/api/types';
|
||||
|
||||
type LoginType = 'dingding' | 'wechat' | 'feishu' | 'oauth' | 'none';
|
||||
|
||||
// 登录选项配置
|
||||
const loginOptions = [
|
||||
{ type: 'none' as LoginType, label: '不启用', disabled: false },
|
||||
{ type: 'dingding' as LoginType, label: '钉钉登录', disabled: false },
|
||||
{ type: 'oauth' as LoginType, label: 'OAuth 登录', disabled: false },
|
||||
{ type: 'wechat' as LoginType, label: '企业微信登录', disabled: true },
|
||||
{ type: 'feishu' as LoginType, label: '飞书登录', disabled: true },
|
||||
];
|
||||
|
||||
// 登录选项组件
|
||||
const LoginOptionButton = ({
|
||||
option,
|
||||
isSelected,
|
||||
onSelect,
|
||||
}: {
|
||||
option: (typeof loginOptions)[0];
|
||||
isSelected: boolean;
|
||||
onSelect: (type: LoginType) => void;
|
||||
}) => (
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
sx={{ gap: 1 }}
|
||||
disabled={option.disabled}
|
||||
onClick={() => !option.disabled && onSelect(option.type)}
|
||||
>
|
||||
<Radio
|
||||
size='small'
|
||||
sx={{ p: 0.5 }}
|
||||
checked={isSelected}
|
||||
disabled={option.disabled}
|
||||
/>
|
||||
{option.label}
|
||||
</Button>
|
||||
);
|
||||
|
||||
const ThirdPartyLoginSettingModal = ({
|
||||
open,
|
||||
onCancel,
|
||||
settingData,
|
||||
onOk,
|
||||
}: {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
settingData: DomainSetting;
|
||||
onOk: () => void;
|
||||
}) => {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
dingtalk_client_id: '',
|
||||
dingtalk_client_secret: '',
|
||||
access_token_url: '',
|
||||
authorize_url: '',
|
||||
client_id: '',
|
||||
client_secret: '',
|
||||
id_field: '',
|
||||
name_field: '',
|
||||
scopes: [] as string[],
|
||||
avatar_field: '',
|
||||
userinfo_url: '',
|
||||
email_field: '',
|
||||
},
|
||||
});
|
||||
|
||||
const [loginType, setLoginType] = useState<LoginType>(
|
||||
settingData?.dingtalk_oauth?.enable ? 'dingding' : 'none'
|
||||
);
|
||||
|
||||
const [scopeInputValue, setScopeInputValue] = useState('');
|
||||
|
||||
const userInfoUrl = watch('userinfo_url');
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
reset();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (settingData?.dingtalk_oauth?.enable) {
|
||||
setLoginType('dingding');
|
||||
reset(
|
||||
{
|
||||
dingtalk_client_id: settingData.dingtalk_oauth.client_id,
|
||||
dingtalk_client_secret: settingData.dingtalk_oauth.client_secret,
|
||||
},
|
||||
{
|
||||
keepValues: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (settingData?.custom_oauth?.enable) {
|
||||
setLoginType('oauth');
|
||||
reset(
|
||||
{
|
||||
access_token_url: settingData.custom_oauth.access_token_url,
|
||||
authorize_url: settingData.custom_oauth.authorize_url,
|
||||
client_id: settingData.custom_oauth.client_id,
|
||||
id_field: settingData.custom_oauth.id_field,
|
||||
name_field: settingData.custom_oauth.name_field,
|
||||
scopes: settingData.custom_oauth.scopes || [],
|
||||
avatar_field: settingData.custom_oauth.avatar_field,
|
||||
userinfo_url: settingData.custom_oauth.userinfo_url,
|
||||
email_field: settingData.custom_oauth.email_field,
|
||||
},
|
||||
{
|
||||
keepValues: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [settingData]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
let params: DomainUpdateSettingReq = {};
|
||||
if (loginType === 'none') {
|
||||
params = {
|
||||
dingtalk_oauth: {
|
||||
enable: false,
|
||||
},
|
||||
custom_oauth: {
|
||||
enable: false,
|
||||
},
|
||||
};
|
||||
} else if (loginType === 'dingding') {
|
||||
params = {
|
||||
dingtalk_oauth: {
|
||||
enable: true,
|
||||
client_id: data.dingtalk_client_id,
|
||||
client_secret: data.dingtalk_client_secret,
|
||||
},
|
||||
custom_oauth: {
|
||||
enable: false,
|
||||
},
|
||||
};
|
||||
} else if (loginType === 'oauth') {
|
||||
params = {
|
||||
dingtalk_oauth: {
|
||||
enable: false,
|
||||
},
|
||||
custom_oauth: {
|
||||
enable: true,
|
||||
access_token_url: data.access_token_url,
|
||||
authorize_url: data.authorize_url,
|
||||
client_id: data.client_id,
|
||||
client_secret: data.client_secret,
|
||||
id_field: data.id_field,
|
||||
name_field: data.name_field,
|
||||
scopes: data.scopes,
|
||||
avatar_field: data.avatar_field,
|
||||
userinfo_url: data.userinfo_url,
|
||||
email_field: data.email_field,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
putUpdateSetting(params).then(() => {
|
||||
message.success('设置成功');
|
||||
onCancel();
|
||||
onOk();
|
||||
});
|
||||
});
|
||||
|
||||
const dingdingForm = () => {
|
||||
return (
|
||||
<>
|
||||
<FormItem label='Client ID' required>
|
||||
<Controller
|
||||
control={control}
|
||||
name='dingtalk_client_id'
|
||||
rules={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Client Id 不能为空',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.dingtalk_client_id}
|
||||
helperText={errors.dingtalk_client_id?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='Client Secret' required>
|
||||
<Controller
|
||||
control={control}
|
||||
name='dingtalk_client_secret'
|
||||
rules={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Client Secret 不能为空',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.dingtalk_client_secret}
|
||||
helperText={errors.dingtalk_client_secret?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const oauthForm = () => {
|
||||
return (
|
||||
<>
|
||||
<FormItem label='Access Token URL' required>
|
||||
<Controller
|
||||
control={control}
|
||||
rules={{
|
||||
required: 'Access Token URL 不能为空',
|
||||
}}
|
||||
name='access_token_url'
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.access_token_url}
|
||||
helperText={errors.access_token_url?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='Authorize URL' required>
|
||||
<Controller
|
||||
control={control}
|
||||
name='authorize_url'
|
||||
rules={{
|
||||
required: 'Authorize URL 不能为空',
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.authorize_url}
|
||||
helperText={errors.authorize_url?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='Client ID' required>
|
||||
<Controller
|
||||
control={control}
|
||||
name='client_id'
|
||||
rules={{
|
||||
required: 'Client ID 不能为空',
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.client_id}
|
||||
helperText={errors.client_id?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='Client Secret' required>
|
||||
<Controller
|
||||
control={control}
|
||||
name='client_secret'
|
||||
rules={{
|
||||
required: 'Client Secret 不能为空',
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.client_secret}
|
||||
helperText={errors.client_secret?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label='Scope' required>
|
||||
<Controller
|
||||
name='scopes'
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
if (value.length === 0) {
|
||||
return 'Scope 不能为空';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Autocomplete
|
||||
multiple
|
||||
id='tags-filled'
|
||||
options={[]}
|
||||
value={field.value}
|
||||
inputValue={scopeInputValue}
|
||||
onChange={(_, value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
onInputChange={(_, value) => {
|
||||
setScopeInputValue(value);
|
||||
}}
|
||||
size='small'
|
||||
freeSolo
|
||||
renderTags={(value: readonly string[], getTagProps) =>
|
||||
value.map((option: string, index: number) => {
|
||||
const { key, ...tagProps } = getTagProps({ index });
|
||||
const label = `${option}`;
|
||||
return (
|
||||
<Chip
|
||||
key={key}
|
||||
label={label}
|
||||
size='small'
|
||||
{...tagProps}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
required
|
||||
placeholder='请输入(可多个, 回车键确认)'
|
||||
error={Boolean(errors.scopes)}
|
||||
helperText={errors.scopes?.message as string}
|
||||
onBlur={() => {
|
||||
// 失去焦点时自动添加当前输入的值
|
||||
const trimmedValue = scopeInputValue.trim();
|
||||
if (trimmedValue && !field.value.includes(trimmedValue)) {
|
||||
field.onChange([...field.value, trimmedValue]);
|
||||
// 清空输入框
|
||||
setScopeInputValue('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='用户信息 URL' required>
|
||||
<Controller
|
||||
control={control}
|
||||
name='userinfo_url'
|
||||
rules={{
|
||||
required: '用户信息 URL 不能为空',
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.userinfo_url}
|
||||
helperText={errors.userinfo_url?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
{userInfoUrl && (
|
||||
<>
|
||||
<FormItem label='ID 字段' required>
|
||||
<Controller
|
||||
control={control}
|
||||
name='id_field'
|
||||
rules={{
|
||||
required: 'ID 字段 不能为空',
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.id_field}
|
||||
helperText={errors.id_field?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='用户名字段' required>
|
||||
<Controller
|
||||
control={control}
|
||||
name='name_field'
|
||||
rules={{
|
||||
required: '用户名字段不能为空',
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.name_field}
|
||||
helperText={errors.name_field?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='头像字段' required>
|
||||
<Controller
|
||||
control={control}
|
||||
name='avatar_field'
|
||||
rules={{
|
||||
required: '头像字段不能为空',
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.avatar_field}
|
||||
helperText={errors.avatar_field?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='邮箱字段' required>
|
||||
<Controller
|
||||
control={control}
|
||||
name='email_field'
|
||||
rules={{
|
||||
required: '邮箱字段不能为空',
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.email_field}
|
||||
helperText={errors.email_field?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
width={800}
|
||||
onCancel={onCancel}
|
||||
title='第三方登录配置'
|
||||
onOk={onSubmit}
|
||||
>
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems='center'
|
||||
spacing={2}
|
||||
sx={{ mt: 2, height: 'calc(100% - 40px)' }}
|
||||
>
|
||||
{loginOptions.map((option) => (
|
||||
<LoginOptionButton
|
||||
key={option.type}
|
||||
option={option}
|
||||
isSelected={loginType === option.type}
|
||||
onSelect={setLoginType}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack gap={2} sx={{ mt: 4 }}>
|
||||
{loginType === 'dingding' && dingdingForm()}
|
||||
{loginType === 'oauth' && oauthForm()}
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThirdPartyLoginSettingModal;
|
||||
149
ui/src/pages/user/chat/chatDetailModal.tsx
Normal file
149
ui/src/pages/user/chat/chatDetailModal.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import Avatar from '@/components/avatar';
|
||||
import Card from '@/components/card';
|
||||
import { getChatInfo } from '@/api/Billing';
|
||||
import MarkDown from '@/components/markDown';
|
||||
import { Ellipsis, Modal } from '@c-x/ui';
|
||||
import { styled } from '@mui/material';
|
||||
import logo from '@/assets/images/logo.png';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { DomainChatContent, DomainChatRecord } from '@/api/types';
|
||||
|
||||
const StyledChatList = styled('div')(() => ({
|
||||
borderRadius: 4,
|
||||
padding: 24,
|
||||
minHeight: 400,
|
||||
maxHeight: 600,
|
||||
overflowY: 'auto',
|
||||
}));
|
||||
|
||||
const StyledChatRow = styled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'isUser',
|
||||
})<{ isUser: boolean }>(({ isUser, theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: isUser ? 'flex-end' : 'flex-start',
|
||||
gap: theme.spacing(1),
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledChatUser = styled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'isUser',
|
||||
})<{ isUser: boolean }>(({ isUser }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: isUser ? 'row-reverse' : 'row',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
}));
|
||||
|
||||
const StyledChatName = styled('div')(({ theme }) => ({
|
||||
color: theme.vars.palette.text.primary,
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
}));
|
||||
|
||||
const StyledChatAvatar = styled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'isUser',
|
||||
})<{ isUser: boolean }>(({ isUser }) => ({
|
||||
margin: isUser ? '0 0 0 12px' : '0 12px 0 0',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
position: 'relative',
|
||||
top: 0,
|
||||
}));
|
||||
|
||||
const StyledChatBubble = styled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'isUser',
|
||||
})<{ isUser: boolean }>(({ isUser }) => ({
|
||||
background: isUser ? '#e6f7ff' : '#f5f5f5',
|
||||
margin: isUser ? '0 36px 0 0' : '0 0 0 36px',
|
||||
borderRadius: 12,
|
||||
padding: '8px 12px',
|
||||
minHeight: 36,
|
||||
maxWidth: 1040,
|
||||
wordBreak: 'break-word',
|
||||
position: 'relative',
|
||||
}));
|
||||
|
||||
const ChatDetailModal = ({
|
||||
data,
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
data?: DomainChatRecord;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const [content, setContent] = useState<DomainChatContent[]>([]);
|
||||
|
||||
const getChatDetailModal = () => {
|
||||
if (!data) return;
|
||||
getChatInfo({ id: data.id! }).then((res) => {
|
||||
setContent(res.contents || []);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) getChatDetailModal();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data, open]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Ellipsis
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
fontSize: 20,
|
||||
lineHeight: '22px',
|
||||
width: 700,
|
||||
}}
|
||||
>
|
||||
对话记录-{data?.user?.username}
|
||||
</Ellipsis>
|
||||
}
|
||||
sx={{
|
||||
'.MuiDialog-paper': {
|
||||
maxWidth: 1300,
|
||||
},
|
||||
}}
|
||||
width={1200}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
>
|
||||
<Card sx={{ p: 0, background: 'transparent', boxShadow: 'none' }}>
|
||||
<StyledChatList>
|
||||
{content.map((item, idx) => {
|
||||
const isUser = item.role === 'user';
|
||||
const name = isUser ? data?.user?.username : 'MonkeyCode';
|
||||
const msg = item.content || '';
|
||||
return (
|
||||
<StyledChatRow key={idx} isUser={isUser}>
|
||||
<StyledChatUser key={idx} isUser={isUser}>
|
||||
<StyledChatAvatar isUser={isUser}>
|
||||
<Avatar
|
||||
name={isUser ? name : undefined}
|
||||
src={isUser ? undefined : logo}
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
fontSize: 16,
|
||||
}}
|
||||
/>
|
||||
</StyledChatAvatar>
|
||||
<StyledChatName>{name}</StyledChatName>
|
||||
</StyledChatUser>
|
||||
<StyledChatBubble isUser={isUser}>
|
||||
<MarkDown content={msg} />
|
||||
</StyledChatBubble>
|
||||
</StyledChatRow>
|
||||
);
|
||||
})}
|
||||
</StyledChatList>
|
||||
</Card>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatDetailModal;
|
||||
170
ui/src/pages/user/chat/index.tsx
Normal file
170
ui/src/pages/user/chat/index.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table } from '@c-x/ui';
|
||||
import { getListChatRecord } from '@/api/Billing';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import Card from '@/components/card';
|
||||
import { Box } from '@mui/material';
|
||||
import StyledLabel from '@/components/label';
|
||||
|
||||
import ChatDetailModal from './chatDetailModal';
|
||||
import { ColumnsType } from '@c-x/ui/dist/Table';
|
||||
import { DomainChatRecord, DomainUser } from '@/api/types';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
import User from '@/components/user';
|
||||
|
||||
const Chat = () => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(20);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dataSource, setDataSource] = useState<DomainChatRecord[]>([]);
|
||||
const [chatDetailModal, setChatDetailModal] = useState<
|
||||
DomainChatRecord | undefined
|
||||
>();
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
const res = await getListChatRecord({
|
||||
page: page,
|
||||
size: size,
|
||||
});
|
||||
setLoading(false);
|
||||
setTotal(res?.total_count || 0);
|
||||
setDataSource(res.records || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
// eslint-disable-next-line
|
||||
}, [page, size]);
|
||||
|
||||
const columns: ColumnsType<DomainChatRecord> = [
|
||||
{
|
||||
dataIndex: 'user',
|
||||
title: '成员',
|
||||
width: 260,
|
||||
render(value: DomainUser) {
|
||||
return (
|
||||
<User
|
||||
id={value.id!}
|
||||
username={value.username!}
|
||||
email={value.email!}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'question',
|
||||
title: '任务内容',
|
||||
render(value: string, record) {
|
||||
const cleanValue = value?.replace(/<\/?task>/g, '') || value;
|
||||
return (
|
||||
<Box
|
||||
onClick={() => setChatDetailModal(record)}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
color: 'info.main',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{cleanValue || '无标题任务'}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'work_mode',
|
||||
title: '工作模式',
|
||||
width: 120,
|
||||
render(value: DomainChatRecord['work_mode']) {
|
||||
const workModeMap: Record<string, Record<string, string>> = {
|
||||
code: {
|
||||
name: '编程模式',
|
||||
color: 'warning',
|
||||
},
|
||||
ask: {
|
||||
name: '问答模式',
|
||||
color: 'info',
|
||||
},
|
||||
architect: {
|
||||
name: '架构模式',
|
||||
color: 'success',
|
||||
},
|
||||
debug: {
|
||||
name: '调试模式',
|
||||
color: 'error',
|
||||
},
|
||||
orchestrator: {
|
||||
name: '编排模式',
|
||||
color: 'info',
|
||||
},
|
||||
};
|
||||
return (
|
||||
<StyledLabel color={value ? workModeMap[value]['color'] : 'default'}>
|
||||
{value ? workModeMap[value]['name'] : '未知'}
|
||||
</StyledLabel>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'input_tokens',
|
||||
title: '输入 Token',
|
||||
width: 150,
|
||||
render(value: number) {
|
||||
return addCommasToNumber(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'output_tokens',
|
||||
title: '输出 Token',
|
||||
width: 150,
|
||||
render(value: number) {
|
||||
return addCommasToNumber(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'created_at',
|
||||
title: '时间',
|
||||
width: 180,
|
||||
render(value: number) {
|
||||
return dayjs.unix(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Card sx={{ flex: 1, height: '100%' }}>
|
||||
<Table
|
||||
height='100%'
|
||||
sx={{ mx: -2 }}
|
||||
PaginationProps={{
|
||||
sx: {
|
||||
pt: 2,
|
||||
mx: 2,
|
||||
},
|
||||
}}
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
rowKey='id'
|
||||
pagination={{
|
||||
page,
|
||||
pageSize: size,
|
||||
total,
|
||||
onChange: (page: number, size: number) => {
|
||||
setPage(page);
|
||||
setSize(size);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ChatDetailModal
|
||||
open={!!chatDetailModal}
|
||||
onClose={() => setChatDetailModal(undefined)}
|
||||
data={chatDetailModal}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
176
ui/src/pages/user/completion/completionDetailModal.tsx
Normal file
176
ui/src/pages/user/completion/completionDetailModal.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import Card from '@/components/card';
|
||||
import { getCompletionInfo } from '@/api/Billing';
|
||||
import { Modal } from '@c-x/ui';
|
||||
import MonacoEditor from '@monaco-editor/react';
|
||||
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { DomainCompletionRecord } from '@/api/types';
|
||||
import { getBaseLanguageId } from '@/utils';
|
||||
|
||||
// 删除 <|im_start|> 和 <|im_end|> 及其间内容的工具函数
|
||||
const removeImBlocks = (text: string) => {
|
||||
// 匹配前后可能的换行符
|
||||
return text.replace(
|
||||
/(^[ \t]*\r?\n)?<\|im_start\|>[\s\S]*?<\|im_end\|>(\r?\n)?/g,
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
const ChatDetailModal = ({
|
||||
data,
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
data?: DomainCompletionRecord;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const [editorValue, setEditorValue] = useState<string>('');
|
||||
const editorRef = useRef<any>(null);
|
||||
const [editorReady, setEditorReady] = useState(false);
|
||||
const [highlightInfo, setHighlightInfo] = useState<any>(null);
|
||||
|
||||
const getChatDetailModal = () => {
|
||||
if (!data) return;
|
||||
getCompletionInfo({ id: data.id! }).then((res) => {
|
||||
// 先去除 <|im_start|> 和 <|im_end|> 及其间内容
|
||||
const rawPrompt = removeImBlocks(res.prompt || '');
|
||||
const content = res.content || '';
|
||||
// 找到三个特殊标记的位置
|
||||
const prefixTag = '<|fim_prefix|>';
|
||||
const suffixTag = '<|fim_suffix|>';
|
||||
const middleTag = '<|fim_middle|>';
|
||||
const prefixIdx = rawPrompt.indexOf(prefixTag);
|
||||
const suffixIdx = rawPrompt.indexOf(suffixTag);
|
||||
const middleIdx = rawPrompt.indexOf(middleTag);
|
||||
// 去掉特殊标记
|
||||
const prompt = rawPrompt
|
||||
.replace(prefixTag, '')
|
||||
.replace(suffixTag, '')
|
||||
.replace(middleTag, '');
|
||||
// 重新定位插入点(因为去掉了前面的 tag,位置会变)
|
||||
// 计算插入点:suffixTag 在原始 prompt 的位置,去掉 prefixTag 后的 offset
|
||||
let insertIdx = suffixIdx;
|
||||
if (prefixIdx !== -1 && prefixIdx < suffixIdx) {
|
||||
insertIdx -= prefixTag.length;
|
||||
}
|
||||
if (middleIdx !== -1 && middleIdx < suffixIdx) {
|
||||
insertIdx -= middleTag.length;
|
||||
}
|
||||
// 插入 content
|
||||
const newValue =
|
||||
prompt.slice(0, insertIdx) + content + prompt.slice(insertIdx);
|
||||
setEditorValue(newValue);
|
||||
// 计算高亮范围(行列)
|
||||
const before = newValue.slice(0, insertIdx);
|
||||
const contentLines = content.split('\n');
|
||||
const beforeLines = before.split('\n');
|
||||
const startLine = beforeLines.length;
|
||||
const startColumn = beforeLines[beforeLines.length - 1].length + 1;
|
||||
const endLine = startLine + contentLines.length - 1;
|
||||
const endColumn =
|
||||
contentLines.length === 1
|
||||
? startColumn + content.length
|
||||
: contentLines[contentLines.length - 1].length + 1;
|
||||
setHighlightInfo({ startLine, startColumn, endLine, endColumn });
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (editorReady && highlightInfo && editorRef.current) {
|
||||
editorRef.current.deltaDecorations(
|
||||
[],
|
||||
[
|
||||
{
|
||||
range: {
|
||||
startLineNumber: highlightInfo.startLine,
|
||||
startColumn: highlightInfo.startColumn,
|
||||
endLineNumber: highlightInfo.endLine,
|
||||
endColumn: highlightInfo.endColumn,
|
||||
},
|
||||
options: {
|
||||
inlineClassName: 'completion-highlight',
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}, [editorReady, highlightInfo, editorValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) getChatDetailModal();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data, open]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title='代码补全'
|
||||
width={800}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
>
|
||||
<Card sx={{ p: 0 }}>
|
||||
<div style={{ height: 420 }}>
|
||||
<MonacoEditor
|
||||
height='100%'
|
||||
language={getBaseLanguageId(data?.program_language || 'plaintext')}
|
||||
value={editorValue}
|
||||
theme='vs-dark'
|
||||
options={{
|
||||
readOnly: true,
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
lineNumbers: 'off',
|
||||
glyphMargin: false,
|
||||
folding: false,
|
||||
overviewRulerLanes: 0,
|
||||
guides: {
|
||||
indentation: true,
|
||||
highlightActiveIndentation: true,
|
||||
highlightActiveBracketPair: false,
|
||||
},
|
||||
renderLineHighlight: 'none',
|
||||
cursorStyle: 'line',
|
||||
cursorBlinking: 'solid',
|
||||
cursorWidth: 0,
|
||||
contextmenu: false,
|
||||
selectionHighlight: false,
|
||||
selectOnLineNumbers: false,
|
||||
occurrencesHighlight: 'off',
|
||||
links: false,
|
||||
hover: { enabled: false },
|
||||
codeLens: false,
|
||||
dragAndDrop: false,
|
||||
mouseWheelZoom: false,
|
||||
accessibilitySupport: 'off',
|
||||
bracketPairColorization: { enabled: false },
|
||||
matchBrackets: 'never',
|
||||
}}
|
||||
onMount={(editor) => {
|
||||
editorRef.current = editor;
|
||||
setEditorReady(true);
|
||||
// 隐藏光标
|
||||
const editorDom = editor.getDomNode();
|
||||
if (editorDom) {
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = `.monaco-editor .cursor { display: none !important; }`;
|
||||
editorDom.appendChild(style);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<style>{`
|
||||
.completion-highlight {
|
||||
background: #264f78 !important;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
`}</style>
|
||||
</Card>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatDetailModal;
|
||||
67
ui/src/pages/user/completion/constant.ts
Normal file
67
ui/src/pages/user/completion/constant.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
export const LANG_OPTIONS = [
|
||||
'JavaScript',
|
||||
'JavaScriptReact',
|
||||
'TypeScript',
|
||||
'TypeScriptReact',
|
||||
'Python',
|
||||
'Java',
|
||||
'C',
|
||||
'C++',
|
||||
'C#',
|
||||
'Go',
|
||||
'PHP',
|
||||
'Ruby',
|
||||
'Swift',
|
||||
'Kotlin',
|
||||
'Rust',
|
||||
'Dart',
|
||||
'Objective-C',
|
||||
'Scala',
|
||||
'Perl',
|
||||
'R',
|
||||
'Shell Script',
|
||||
'PowerShell',
|
||||
'HTML',
|
||||
'CSS',
|
||||
'SCSS',
|
||||
'Less',
|
||||
'JSON',
|
||||
'YAML',
|
||||
'XML',
|
||||
'Markdown',
|
||||
'SQL',
|
||||
'GraphQL',
|
||||
'Dockerfile',
|
||||
'Makefile',
|
||||
'Lua',
|
||||
'Haskell',
|
||||
'Elixir',
|
||||
'Erlang',
|
||||
'F#',
|
||||
'Groovy',
|
||||
'Visual Basic',
|
||||
'Assembly',
|
||||
'Matlab',
|
||||
'Fortran',
|
||||
'COBOL',
|
||||
'Prolog',
|
||||
'Scheme',
|
||||
'Lisp',
|
||||
'Julia',
|
||||
'SASS',
|
||||
'TOML',
|
||||
'INI',
|
||||
'LaTeX',
|
||||
'CMake',
|
||||
'Batch',
|
||||
'CoffeeScript',
|
||||
'Crystal',
|
||||
'OCaml',
|
||||
'Nim',
|
||||
'ReScript',
|
||||
'Solidity',
|
||||
'Vue',
|
||||
'Svelte',
|
||||
'JSX',
|
||||
'TSX',
|
||||
];
|
||||
237
ui/src/pages/user/completion/index.tsx
Normal file
237
ui/src/pages/user/completion/index.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { DomainCompletionRecord, DomainUser } from '@/api/types';
|
||||
import { getListCompletionRecord } from '@/api/Billing';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { Table } from '@c-x/ui';
|
||||
import Card from '@/components/card';
|
||||
import {
|
||||
Box,
|
||||
Stack,
|
||||
MenuItem,
|
||||
Select,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import { getListUser } from '@/api/User';
|
||||
import dayjs from 'dayjs';
|
||||
import { useDebounceFn } from 'ahooks';
|
||||
import { ColumnsType } from '@c-x/ui/dist/Table';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
import CompletionDetailModal from './completionDetailModal';
|
||||
import StyledLabel from '@/components/label';
|
||||
import { LANG_OPTIONS } from './constant';
|
||||
import User from '@/components/user';
|
||||
|
||||
const Completion = () => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(20);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [dataSource, setDataSource] = useState<DomainCompletionRecord[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [completionDetailModal, setCompletionDetailModal] = useState<
|
||||
DomainCompletionRecord | undefined
|
||||
>();
|
||||
|
||||
// 新增筛选项 state
|
||||
const [filterUser, setFilterUser] = useState('');
|
||||
const [filterLang, setFilterLang] = useState('');
|
||||
const [filterAccept, setFilterAccept] = useState<
|
||||
'accepted' | 'unaccepted' | ''
|
||||
>('accepted');
|
||||
|
||||
const { data: userOptions = { users: [] } } = useRequest(() =>
|
||||
getListUser({
|
||||
page: 1,
|
||||
size: 10,
|
||||
})
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1); // 筛选变化时重置页码
|
||||
fetchData({
|
||||
page: 1,
|
||||
language: filterLang,
|
||||
author: filterUser,
|
||||
is_accept: filterAccept,
|
||||
});
|
||||
}, [filterUser, filterLang, filterAccept]);
|
||||
|
||||
const fetchData = async (params: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
language?: string;
|
||||
author?: string;
|
||||
is_accept?: 'accepted' | 'unaccepted' | '';
|
||||
}) => {
|
||||
setLoading(true);
|
||||
const isAccept = params.is_accept || filterAccept;
|
||||
const res = await getListCompletionRecord({
|
||||
page: params.page || page,
|
||||
size: params.size || size,
|
||||
language: params.language || filterLang,
|
||||
author: params.author || filterUser,
|
||||
is_accept:
|
||||
isAccept === 'accepted'
|
||||
? true
|
||||
: isAccept === 'unaccepted'
|
||||
? false
|
||||
: undefined,
|
||||
});
|
||||
setLoading(false);
|
||||
setTotal(res?.total_count || 0);
|
||||
setDataSource(res.records || []);
|
||||
};
|
||||
|
||||
const columns: ColumnsType<DomainCompletionRecord> = [
|
||||
{
|
||||
dataIndex: 'user',
|
||||
title: '成员',
|
||||
render(value: DomainUser) {
|
||||
return (
|
||||
<User
|
||||
id={value.id!}
|
||||
username={value.username!}
|
||||
email={value.email!}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'task',
|
||||
title: '补全内容',
|
||||
width: 150,
|
||||
render(_, record) {
|
||||
return (
|
||||
<Box
|
||||
onClick={() => setCompletionDetailModal(record)}
|
||||
sx={{ color: 'info.main', cursor: 'pointer' }}
|
||||
>
|
||||
点击查看
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'is_accept',
|
||||
title: '是否采纳',
|
||||
width: 130,
|
||||
render(value: boolean) {
|
||||
const color = value ? 'success' : 'default';
|
||||
return (
|
||||
<StyledLabel color={color}>{value ? '已采纳' : '未采纳'}</StyledLabel>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'program_language',
|
||||
title: '编程语言',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
dataIndex: 'input_tokens',
|
||||
title: '输入 Token',
|
||||
width: 140,
|
||||
render(value: number) {
|
||||
return addCommasToNumber(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'output_tokens',
|
||||
title: '输出 Token',
|
||||
width: 140,
|
||||
render(value: number) {
|
||||
return addCommasToNumber(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'created_at',
|
||||
title: '时间',
|
||||
width: 200,
|
||||
render(value: number) {
|
||||
return dayjs.unix(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const debounceSetFilterLang = useDebounceFn(
|
||||
(val: string) => setFilterLang(val),
|
||||
{
|
||||
wait: 500,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Card sx={{ flex: 1, height: '100%' }}>
|
||||
<Stack direction='row' spacing={2} sx={{ mb: 2 }}>
|
||||
<Autocomplete
|
||||
size='small'
|
||||
sx={{ minWidth: 220 }}
|
||||
options={LANG_OPTIONS}
|
||||
getOptionLabel={(option) => option || ''}
|
||||
value={filterLang || ''}
|
||||
freeSolo
|
||||
onChange={(_, newValue) => {
|
||||
setFilterLang(newValue ? String(newValue) : '');
|
||||
}}
|
||||
onInputChange={(_, newInputValue) =>
|
||||
debounceSetFilterLang.run(newInputValue)
|
||||
}
|
||||
renderInput={(params) => <TextField {...params} label='语言' />}
|
||||
clearOnEscape
|
||||
/>
|
||||
<FormControl size='small' sx={{ minWidth: 180 }}>
|
||||
<InputLabel>是否采纳</InputLabel>
|
||||
<Select
|
||||
label='是否采纳'
|
||||
value={filterAccept}
|
||||
onChange={(e) =>
|
||||
setFilterAccept(e.target.value as 'accepted' | 'unaccepted')
|
||||
}
|
||||
>
|
||||
<MenuItem value=''>全部</MenuItem>
|
||||
<MenuItem value='accepted'>已采纳</MenuItem>
|
||||
<MenuItem value='unaccepted'>未采纳</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
<Table
|
||||
size='large'
|
||||
height='calc(100% - 56px)'
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={dataSource || []}
|
||||
sx={{ mx: -2 }}
|
||||
PaginationProps={{
|
||||
sx: {
|
||||
pt: 2,
|
||||
mx: 2,
|
||||
},
|
||||
}}
|
||||
rowKey='id'
|
||||
pagination={{
|
||||
pageSize: size,
|
||||
total: total,
|
||||
page: page,
|
||||
onChange: (page, size) => {
|
||||
setPage(page);
|
||||
setSize(size);
|
||||
fetchData({
|
||||
page,
|
||||
size,
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<CompletionDetailModal
|
||||
open={!!completionDetailModal}
|
||||
onClose={() => setCompletionDetailModal(undefined)}
|
||||
data={completionDetailModal}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Completion;
|
||||
212
ui/src/pages/user/dashboard/components/memberStatistic.tsx
Normal file
212
ui/src/pages/user/dashboard/components/memberStatistic.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Grid2 as Grid } from '@mui/material';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import MemberInfo from '@/pages/dashboard/components/memberInfo';
|
||||
import PieCharts from '@/pages/dashboard/components/pieCharts';
|
||||
import LineCharts from '@/pages/dashboard/components/lineCharts';
|
||||
import { RecentActivityCard } from '@/pages/dashboard/components/statisticCard';
|
||||
import { useRequest } from 'ahooks';
|
||||
import {
|
||||
getUserEventsDashboard,
|
||||
getUserStatDashboard,
|
||||
getUserHeatmapDashboard,
|
||||
} from '@/api/Dashboard';
|
||||
import { StyledHighlight } from '@/pages/dashboard/components/globalStatistic';
|
||||
import { getRecent90DaysData, getRecent24HoursData } from '@/utils';
|
||||
import { DomainUser } from '@/api/types';
|
||||
import { TimeRange } from '../index';
|
||||
|
||||
interface TimeDuration {
|
||||
duration: number;
|
||||
precision: 'day' | 'hour';
|
||||
}
|
||||
|
||||
const MemberStatistic = ({
|
||||
memberData,
|
||||
timeRange,
|
||||
}: {
|
||||
memberData: DomainUser | null;
|
||||
timeRange: TimeRange;
|
||||
}) => {
|
||||
const [timeDuration, setTimeDuration] = useState<TimeDuration>({
|
||||
duration: timeRange === '90d' ? 90 : 24,
|
||||
precision: timeRange === '90d' ? 'day' : 'hour',
|
||||
});
|
||||
|
||||
const { id } = useParams();
|
||||
const { data: userEvents } = useRequest(
|
||||
() =>
|
||||
getUserEventsDashboard({
|
||||
user_id: id || '',
|
||||
precision: timeDuration.precision,
|
||||
}),
|
||||
{
|
||||
refreshDeps: [id],
|
||||
manual: false,
|
||||
ready: !!id,
|
||||
}
|
||||
);
|
||||
const { data: userStat } = useRequest(
|
||||
() =>
|
||||
getUserStatDashboard({
|
||||
user_id: id || '',
|
||||
...timeDuration,
|
||||
}),
|
||||
{
|
||||
refreshDeps: [id, timeDuration],
|
||||
manual: false,
|
||||
ready: !!id,
|
||||
}
|
||||
);
|
||||
const { data: userHeatmap } = useRequest(
|
||||
() =>
|
||||
getUserHeatmapDashboard({
|
||||
user_id: id || '',
|
||||
}),
|
||||
{
|
||||
refreshDeps: [id],
|
||||
manual: false,
|
||||
ready: !!id,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeDuration({
|
||||
duration: timeRange === '90d' ? 90 : 24,
|
||||
precision: timeRange === '90d' ? 'day' : 'hour',
|
||||
});
|
||||
}, [timeRange]);
|
||||
|
||||
const getRangeData = (
|
||||
data: Record<string, number>[],
|
||||
timeRange: TimeRange,
|
||||
label: { keyLabel?: string; valueLabel?: string } = { valueLabel: 'value' }
|
||||
) => {
|
||||
return timeRange === '90d'
|
||||
? getRecent90DaysData(data, label)
|
||||
: getRecent24HoursData(data, label);
|
||||
};
|
||||
|
||||
const {
|
||||
chatChartData,
|
||||
codeCompletionChartData,
|
||||
codeLineChartData,
|
||||
acceptedPerChartData,
|
||||
} = useMemo(() => {
|
||||
const {
|
||||
accepted_per = [],
|
||||
chats = [],
|
||||
code_completions = [],
|
||||
lines_of_code = [],
|
||||
} = userStat || {};
|
||||
const label = { valueLabel: 'value' };
|
||||
const chatChartData = getRangeData(chats, timeRange, label);
|
||||
const codeCompletionChartData = getRangeData(
|
||||
code_completions,
|
||||
timeRange,
|
||||
label
|
||||
);
|
||||
const codeLineChartData = getRangeData(lines_of_code, timeRange, label);
|
||||
const acceptedPerChartData = getRangeData(accepted_per, timeRange, label);
|
||||
return {
|
||||
chatChartData,
|
||||
codeCompletionChartData,
|
||||
codeLineChartData,
|
||||
acceptedPerChartData,
|
||||
};
|
||||
}, [userStat]);
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={2}
|
||||
sx={{
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
>
|
||||
<Grid container size={9}>
|
||||
<Grid size={12}>
|
||||
<MemberInfo data={userHeatmap || {}} memberData={memberData} />
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<PieCharts
|
||||
title='工作模式-对话任务'
|
||||
extra={timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}
|
||||
data={userStat?.work_mode || []}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<PieCharts
|
||||
title='编程语言'
|
||||
extra={timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}
|
||||
data={userStat?.program_language || []}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<RecentActivityCard data={userEvents || []} />
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<LineCharts
|
||||
title='对话任务'
|
||||
data={chatChartData}
|
||||
extra={
|
||||
<>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共
|
||||
<StyledHighlight>{userStat?.total_chats || 0}</StyledHighlight>
|
||||
个对话任务
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<LineCharts
|
||||
title='补全任务'
|
||||
data={codeCompletionChartData}
|
||||
extra={
|
||||
<>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共
|
||||
<StyledHighlight>
|
||||
{userStat?.total_completions || 0}
|
||||
</StyledHighlight>
|
||||
个补全任务
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<LineCharts
|
||||
title='代码量'
|
||||
data={codeLineChartData}
|
||||
extra={
|
||||
<>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共修改
|
||||
<StyledHighlight>
|
||||
{userStat?.total_lines_of_code || 0}
|
||||
</StyledHighlight>
|
||||
行代码
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<LineCharts
|
||||
title='补全任务采纳率'
|
||||
data={acceptedPerChartData}
|
||||
extra={
|
||||
<>
|
||||
{timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}平均采纳率为
|
||||
<StyledHighlight>
|
||||
{(userStat?.total_accepted_per || 0).toFixed(2)}
|
||||
</StyledHighlight>
|
||||
%
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemberStatistic;
|
||||
72
ui/src/pages/user/dashboard/index.tsx
Normal file
72
ui/src/pages/user/dashboard/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { getListUser } from '@/api/User';
|
||||
import { Stack, MenuItem, Select } from '@mui/material';
|
||||
|
||||
import { useRequest } from 'ahooks';
|
||||
import MemberStatistic from './components/memberStatistic';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { DomainUser } from '@/api/types';
|
||||
|
||||
export type TimeRange = '90d' | '24h';
|
||||
|
||||
const Dashboard = () => {
|
||||
const navigate = useNavigate();
|
||||
const { tab, id } = useParams();
|
||||
const [tabValue, setTabValue] = useState(tab || 'global');
|
||||
const [memberData, setMemberData] = useState<DomainUser | null>(null);
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>('24h');
|
||||
|
||||
const { data: userData, refresh } = useRequest(
|
||||
() =>
|
||||
getListUser({
|
||||
page: 1,
|
||||
size: 99999,
|
||||
}),
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: (res) => {
|
||||
if (id) {
|
||||
setMemberData(res.users?.find((item) => item.id === id) || null);
|
||||
} else {
|
||||
setMemberData(res.users?.[0] || null);
|
||||
navigate(`/dashboard/member/${res.users?.[0]?.id}`);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
const userList = useMemo(() => {
|
||||
return userData?.users || [];
|
||||
}, [userData]);
|
||||
useEffect(() => {
|
||||
if (tabValue === 'member') {
|
||||
refresh();
|
||||
}
|
||||
}, [tabValue]);
|
||||
|
||||
const onMemberChange = (data: DomainUser) => {
|
||||
setMemberData(data);
|
||||
navigate(`/dashboard/member/${data.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={2} sx={{ height: '100%' }}>
|
||||
<Stack direction='row' justifyContent='space-between' alignItems='center'>
|
||||
<Select
|
||||
labelId='time-range-label'
|
||||
value={timeRange}
|
||||
size='small'
|
||||
onChange={(e) => setTimeRange(e.target.value as TimeRange)}
|
||||
sx={{ fontSize: 14 }}
|
||||
>
|
||||
<MenuItem value='24h'>最近 24 小时</MenuItem>
|
||||
<MenuItem value='90d'>最近 90 天</MenuItem>
|
||||
</Select>
|
||||
</Stack>
|
||||
|
||||
<MemberStatistic memberData={memberData} timeRange={timeRange} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
343
ui/src/pages/user/login/index.tsx
Normal file
343
ui/src/pages/user/login/index.tsx
Normal file
@@ -0,0 +1,343 @@
|
||||
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import Logo from '@/assets/images/logo.png';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
TextField,
|
||||
Typography,
|
||||
Container,
|
||||
Paper,
|
||||
CircularProgress,
|
||||
Grid2 as Grid,
|
||||
InputAdornment,
|
||||
IconButton,
|
||||
Divider,
|
||||
Stack,
|
||||
} from '@mui/material';
|
||||
import { Icon, message } from '@c-x/ui';
|
||||
|
||||
import { getRedirectUrl } from '@/utils';
|
||||
|
||||
// @ts-ignore
|
||||
import { AestheticFluidBg } from '@/assets/jsm/AestheticFluidBg.module.js';
|
||||
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { postLogin, getUserOauthSignupOrIn } from '@/api/User';
|
||||
import { getGetSetting } from '@/api/Admin';
|
||||
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { DomainSetting } from '@/api/types';
|
||||
|
||||
// 样式化组件
|
||||
const StyledContainer = styled(Container)(({ theme }) => ({
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
maxWidth: '100% !important',
|
||||
background: theme.palette.background.paper,
|
||||
}));
|
||||
|
||||
const StyledPaper = styled(Paper)(({ theme }) => ({
|
||||
position: 'relative',
|
||||
zIndex: 9,
|
||||
padding: theme.spacing(4),
|
||||
background: 'rgba(255, 255, 255, 0.85)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
width: 458,
|
||||
borderRadius: theme.spacing(2),
|
||||
boxShadow:
|
||||
'0px 0px 4px 0px rgba(54,59,76,0.1), 0px 20px 40px 0px rgba(54,59,76,0.1)',
|
||||
}));
|
||||
|
||||
const LogoContainer = styled(Box)(({ theme }) => ({
|
||||
textAlign: 'center',
|
||||
marginBottom: theme.spacing(4),
|
||||
}));
|
||||
|
||||
const LogoImage = styled('img')({
|
||||
width: 48,
|
||||
height: 48,
|
||||
});
|
||||
|
||||
const LogoTitle = styled(Typography)(({ theme }) => ({
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: theme.palette.primary.main,
|
||||
}));
|
||||
|
||||
const StyledTextField = styled(TextField)(({ theme }) => ({
|
||||
'.MuiInputBase-root': {
|
||||
backgroundColor: '#fff',
|
||||
paddingLeft: '20px',
|
||||
},
|
||||
'.MuiInputBase-input': {
|
||||
paddingTop: '16px',
|
||||
paddingBottom: '16px',
|
||||
fontSize: 14,
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledButton = styled(Button)(({ theme }) => ({
|
||||
height: 48,
|
||||
textTransform: 'none',
|
||||
}));
|
||||
|
||||
const IconWrapper = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: theme.palette.text.primary,
|
||||
marginRight: theme.spacing(2),
|
||||
fontSize: 16,
|
||||
}));
|
||||
|
||||
const TogglePasswordIcon = styled(Icon)({
|
||||
fontSize: 20,
|
||||
});
|
||||
|
||||
// 表单数据类型
|
||||
interface LoginFormData {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// 背景动画配置
|
||||
const BACKGROUND_CONFIG = {
|
||||
dom: 'box',
|
||||
colors: ['#FDFDFD', '#DDDDDD', '#BBBBBB', '#555555', '#343434', '#010101'],
|
||||
loop: true,
|
||||
} as const;
|
||||
|
||||
const UserLogin = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const { data: loginSetting = {} as DomainSetting } =
|
||||
useRequest(getGetSetting);
|
||||
const { custom_oauth = {}, dingtalk_oauth = {} } = loginSetting;
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<LoginFormData>();
|
||||
|
||||
// 切换密码显示状态
|
||||
const togglePasswordVisibility = useCallback(() => {
|
||||
setShowPassword((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
// 处理登录表单提交
|
||||
const onSubmit = useCallback(
|
||||
async (data: LoginFormData) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const sessionId = searchParams.get('session_id');
|
||||
if (!sessionId) {
|
||||
message.error('缺少会话ID参数');
|
||||
return;
|
||||
}
|
||||
|
||||
// 用户登录
|
||||
const loginResult = await postLogin({
|
||||
...data,
|
||||
session_id: sessionId,
|
||||
});
|
||||
|
||||
window.location.href = loginResult.redirect_url!;
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : '登录失败,请重试';
|
||||
setError(errorMessage);
|
||||
console.error('登录失败:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[searchParams]
|
||||
);
|
||||
|
||||
// 初始化背景动画
|
||||
useEffect(() => {
|
||||
new AestheticFluidBg(BACKGROUND_CONFIG);
|
||||
}, []);
|
||||
|
||||
const oauthEnable = useMemo(() => {
|
||||
return (
|
||||
loginSetting.custom_oauth?.enable || loginSetting.dingtalk_oauth?.enable
|
||||
);
|
||||
}, [loginSetting]);
|
||||
|
||||
// 渲染用户名输入框
|
||||
const renderUsernameField = () => (
|
||||
<Controller
|
||||
name='username'
|
||||
control={control}
|
||||
defaultValue=''
|
||||
rules={{ required: '请输入用户名' }}
|
||||
render={({ field }) => (
|
||||
<StyledTextField
|
||||
{...field}
|
||||
fullWidth
|
||||
placeholder='请输入用户名'
|
||||
variant='outlined'
|
||||
error={!!errors.username}
|
||||
helperText={errors.username?.message}
|
||||
disabled={loading}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<IconWrapper>
|
||||
<Icon type='icon-zhanghao' />
|
||||
</IconWrapper>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
// 渲染密码输入框
|
||||
const renderPasswordField = () => (
|
||||
<Controller
|
||||
name='password'
|
||||
control={control}
|
||||
defaultValue=''
|
||||
rules={{ required: '请输入密码' }}
|
||||
render={({ field }) => (
|
||||
<StyledTextField
|
||||
{...field}
|
||||
fullWidth
|
||||
placeholder='请输入密码'
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
variant='outlined'
|
||||
error={!!errors.password}
|
||||
helperText={errors.password?.message}
|
||||
disabled={loading}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<IconWrapper>
|
||||
<Icon type='icon-mima' />
|
||||
</IconWrapper>
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position='end'>
|
||||
<IconButton
|
||||
aria-label='切换密码显示'
|
||||
onClick={togglePasswordVisibility}
|
||||
edge='end'
|
||||
disabled={loading}
|
||||
size='small'
|
||||
>
|
||||
<TogglePasswordIcon
|
||||
type={showPassword ? 'icon-kejian' : 'icon-bukejian'}
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
// 渲染登录按钮
|
||||
const renderLoginButton = () => (
|
||||
<Grid size={12}>
|
||||
<StyledButton
|
||||
type='submit'
|
||||
fullWidth
|
||||
variant='contained'
|
||||
size='large'
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? <CircularProgress size={18} /> : '登录'}
|
||||
</StyledButton>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const onOauthLogin = (platform: 'dingtalk' | 'custom') => {
|
||||
const redirectUrl = getRedirectUrl();
|
||||
getUserOauthSignupOrIn({
|
||||
platform,
|
||||
redirect_url: redirectUrl.href,
|
||||
}).then((res) => {
|
||||
if (res.url) {
|
||||
window.location.href = res.url;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const oauthLogin = () => {
|
||||
return (
|
||||
<Stack justifyContent='center'>
|
||||
<Divider sx={{ my: 3, fontSize: 12, borderColor: 'divider' }}>
|
||||
使用其他方式登录
|
||||
</Divider>
|
||||
{dingtalk_oauth.enable && (
|
||||
<IconButton
|
||||
sx={{ alignSelf: 'center' }}
|
||||
onClick={() => onOauthLogin('dingtalk')}
|
||||
>
|
||||
<Icon type='icon-dingding' sx={{ fontSize: 30 }} />
|
||||
</IconButton>
|
||||
)}
|
||||
{custom_oauth.enable && (
|
||||
<IconButton
|
||||
sx={{ alignSelf: 'center' }}
|
||||
onClick={() => onOauthLogin('custom')}
|
||||
>
|
||||
<Icon type='icon-oauth' sx={{ fontSize: 30 }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染登录表单
|
||||
const renderLoginForm = () => (
|
||||
<>
|
||||
<Box component='form' onSubmit={handleSubmit(onSubmit)}>
|
||||
<Grid container spacing={4}>
|
||||
<Grid size={12}>{renderUsernameField()}</Grid>
|
||||
<Grid size={12}>{renderPasswordField()}</Grid>
|
||||
|
||||
{renderLoginButton()}
|
||||
</Grid>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const redirect_url = searchParams.get('redirect_url');
|
||||
if (redirect_url) {
|
||||
window.location.href = redirect_url;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StyledContainer id='box'>
|
||||
<StyledPaper elevation={3}>
|
||||
<LogoContainer>
|
||||
<LogoImage src={Logo} alt='Monkey Code Logo' />
|
||||
<LogoTitle variant='h4' gutterBottom>
|
||||
Monkey Code
|
||||
</LogoTitle>
|
||||
</LogoContainer>
|
||||
{!loginSetting.disable_password_login && renderLoginForm()}
|
||||
{oauthEnable && oauthLogin()}
|
||||
</StyledPaper>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserLogin;
|
||||
@@ -1,215 +0,0 @@
|
||||
import { Button, Radio, Stack, Box, TextField } from '@mui/material';
|
||||
import { Modal, Icon, message } from '@c-x/ui';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { StyledFormLabel } from '@/components/form';
|
||||
import { putUpdateSetting } from '@/api/User';
|
||||
import { DomainSetting } from '@/api/types';
|
||||
|
||||
type LoginType = 'dingding' | 'wechat' | 'feishu' | 'oauth' | 'none';
|
||||
|
||||
const ThirdPartyLoginSettingModal = ({
|
||||
open,
|
||||
onCancel,
|
||||
settingData,
|
||||
onOk,
|
||||
}: {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
settingData: DomainSetting;
|
||||
onOk: () => void;
|
||||
}) => {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
dingtalk_client_id: '',
|
||||
dingtalk_client_secret: '',
|
||||
// title: '',
|
||||
},
|
||||
});
|
||||
|
||||
const [loginType, setLoginType] = useState<LoginType>(
|
||||
settingData?.enable_dingtalk_oauth ? 'dingding' : 'none'
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
reset();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (settingData?.enable_dingtalk_oauth) {
|
||||
setLoginType('dingding');
|
||||
}
|
||||
}, [settingData]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
if (loginType === 'none') {
|
||||
putUpdateSetting({ ...data, enable_dingtalk_oauth: false }).then(() => {
|
||||
message.success('设置成功');
|
||||
onCancel();
|
||||
onOk();
|
||||
});
|
||||
}
|
||||
if (loginType === 'dingding') {
|
||||
putUpdateSetting({ ...data, enable_dingtalk_oauth: true }).then(() => {
|
||||
message.success('设置成功');
|
||||
onCancel();
|
||||
onOk();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
width={900}
|
||||
onCancel={onCancel}
|
||||
title='第三方登录配置'
|
||||
onOk={onSubmit}
|
||||
>
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems='center'
|
||||
spacing={2}
|
||||
sx={{ mt: 2, height: 'calc(100% - 40px)' }}
|
||||
>
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
sx={{ gap: 2 }}
|
||||
onClick={() => {
|
||||
setLoginType('none');
|
||||
}}
|
||||
>
|
||||
<Radio size='small' sx={{ p: 0.5 }} checked={loginType === 'none'} />
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
不启用
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Button>
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
sx={{ gap: 2 }}
|
||||
onClick={() => {
|
||||
setLoginType('dingding');
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
size='small'
|
||||
sx={{ p: 0.5 }}
|
||||
checked={loginType === 'dingding'}
|
||||
/>
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
钉钉登录
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Button>
|
||||
<Button variant='outlined' color='primary' sx={{ gap: 2 }} disabled>
|
||||
<Radio size='small' sx={{ p: 0.5 }} disabled />
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
企业微信登录
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Button>
|
||||
<Button variant='outlined' color='primary' sx={{ gap: 2 }} disabled>
|
||||
<Radio size='small' sx={{ p: 0.5 }} disabled />
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
飞书登录
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Button>
|
||||
<Button variant='outlined' color='primary' sx={{ gap: 2 }} disabled>
|
||||
<Radio size='small' sx={{ p: 0.5 }} disabled />
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
OAuth 登录
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Stack>
|
||||
{loginType === 'dingding' && (
|
||||
<Stack gap={2} sx={{ mt: 4 }}>
|
||||
<Box>
|
||||
<StyledFormLabel required>Client ID</StyledFormLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name='dingtalk_client_id'
|
||||
rules={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Client Id 不能为空',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.dingtalk_client_id}
|
||||
helperText={errors.dingtalk_client_id?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<StyledFormLabel required>Client Secret</StyledFormLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name='dingtalk_client_secret'
|
||||
rules={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Client Secret 不能为空',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.dingtalk_client_secret}
|
||||
helperText={errors.dingtalk_client_secret?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
{/* <Box>
|
||||
<StyledFormLabel>标题名称,默认为 身份认证-钉钉登录</StyledFormLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name='title'
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='请输入'
|
||||
error={!!errors.title}
|
||||
helperText={errors.title?.message}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box> */}
|
||||
</Stack>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThirdPartyLoginSettingModal;
|
||||
@@ -32,18 +32,26 @@ const Dashboard = LazyLoadable(lazy(() => import('@/pages/dashboard')));
|
||||
const Chat = LazyLoadable(lazy(() => import('@/pages/chat')));
|
||||
const Completion = LazyLoadable(lazy(() => import('@/pages/completion')));
|
||||
const Model = LazyLoadable(lazy(() => import('@/pages/model')));
|
||||
const User = LazyLoadable(lazy(() => import('@/pages/user')));
|
||||
const User = LazyLoadable(lazy(() => import('@/pages/user-management')));
|
||||
const Admin = LazyLoadable(lazy(() => import('@/pages/admin')));
|
||||
const Invite = LazyLoadable(lazy(() => import('@/pages/invite')));
|
||||
const Auth = LazyLoadable(lazy(() => import('@/pages/auth')));
|
||||
const Login = LazyLoadable(lazy(() => import('@/pages/login')));
|
||||
const UserLogin = LazyLoadable(lazy(() => import('@/pages/user/login')));
|
||||
const Expectation = LazyLoadable(lazy(() => import('@/pages/expectation')));
|
||||
const UserChat = LazyLoadable(lazy(() => import('@/pages/user/chat')));
|
||||
const UserCompletion = LazyLoadable(
|
||||
lazy(() => import('@/pages/user/completion'))
|
||||
);
|
||||
|
||||
const UserDashboard = LazyLoadable(
|
||||
lazy(() => import('@/pages/user/dashboard'))
|
||||
);
|
||||
|
||||
const routerConfig = [
|
||||
{
|
||||
path: '/',
|
||||
element: <MainLayout />,
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -70,7 +78,7 @@ const routerConfig = [
|
||||
element: <Model />,
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
path: 'user-management',
|
||||
element: <User />,
|
||||
},
|
||||
{
|
||||
@@ -79,6 +87,29 @@ const routerConfig = [
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: '/user',
|
||||
element: <MainLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Navigate to='/user/dashboard' replace />,
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
element: <UserDashboard />,
|
||||
},
|
||||
{
|
||||
path: 'chat',
|
||||
element: <UserChat />,
|
||||
},
|
||||
{
|
||||
path: 'completion',
|
||||
element: <UserCompletion />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/invite/:id/:step?',
|
||||
element: <Invite />,
|
||||
@@ -87,6 +118,10 @@ const routerConfig = [
|
||||
path: '/auth',
|
||||
element: <Auth />,
|
||||
},
|
||||
{
|
||||
path: '/user/login',
|
||||
element: <UserLogin />,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
element: <Login />,
|
||||
|
||||
Reference in New Issue
Block a user