Merge pull request #116 from guanweiwang/main

feat: 添加用户界面
This commit is contained in:
Yoko
2025-07-21 18:51:18 +08:00
committed by GitHub
18 changed files with 508 additions and 100 deletions

View File

@@ -40,7 +40,9 @@ export enum ContentType {
const redirectToLogin = () => {
const redirectAfterLogin = encodeURIComponent(location.href);
const search = `redirect=${redirectAfterLogin}`;
const pathname = '/login';
const pathname = location.pathname.startsWith('/user')
? '/user/login'
: '/login';
window.location.href = `${pathname}?${search}`;
};

110
ui/src/api/UserDashboard.ts Normal file
View File

@@ -0,0 +1,110 @@
/* 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 {
DomainUserEvent,
DomainUserHeatmapResp,
DomainUserStat,
GetUserDashboardEventsParams,
GetUserDashboardStatParams,
WebResp,
} from "./types";
/**
* @description 获取用户事件
*
* @tags User Dashboard
* @name GetUserDashboardEvents
* @summary 获取用户事件
* @request GET:/api/v1/user/dashboard/events
* @response `200` `(WebResp & {
data?: (DomainUserEvent)[],
})` OK
* @response `401` `string` Unauthorized
*/
export const getUserDashboardEvents = (
query: GetUserDashboardEventsParams,
params: RequestParams = {},
) =>
request<
WebResp & {
data?: DomainUserEvent[];
}
>({
path: `/api/v1/user/dashboard/events`,
method: "GET",
query: query,
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description 用户热力图
*
* @tags User Dashboard
* @name GetUserDashboardHeatmap
* @summary 用户热力图
* @request GET:/api/v1/user/dashboard/heatmap
* @response `200` `(WebResp & {
data?: DomainUserHeatmapResp,
})` OK
* @response `401` `string` Unauthorized
*/
export const getUserDashboardHeatmap = (params: RequestParams = {}) =>
request<
WebResp & {
data?: DomainUserHeatmapResp;
}
>({
path: `/api/v1/user/dashboard/heatmap`,
method: "GET",
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description 获取用户统计信息
*
* @tags User Dashboard
* @name GetUserDashboardStat
* @summary 获取用户统计信息
* @request GET:/api/v1/user/dashboard/stat
* @response `200` `(WebResp & {
data?: DomainUserStat,
})` OK
* @response `401` `string` Unauthorized
*/
export const getUserDashboardStat = (
query: GetUserDashboardStatParams,
params: RequestParams = {},
) =>
request<
WebResp & {
data?: DomainUserStat;
}
>({
path: `/api/v1/user/dashboard/stat`,
method: "GET",
query: query,
type: ContentType.Json,
format: "json",
...params,
});

72
ui/src/api/UserManage.ts Normal file
View File

@@ -0,0 +1,72 @@
/* 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 { DomainProfileUpdateReq, DomainUser, WebResp } from "./types";
/**
* @description 获取用户信息
*
* @tags User Manage
* @name GetUserProfile
* @summary 获取用户信息
* @request GET:/api/v1/user/profile
* @response `200` `(WebResp & {
data?: DomainUser,
})` OK
* @response `401` `WebResp` Unauthorized
*/
export const getUserProfile = (params: RequestParams = {}) =>
request<
WebResp & {
data?: DomainUser;
}
>({
path: `/api/v1/user/profile`,
method: "GET",
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description 更新用户信息
*
* @tags User Manage
* @name PutUserUpdateProfile
* @summary 更新用户信息
* @request PUT:/api/v1/user/profile
* @response `200` `(WebResp & {
data?: DomainUser,
})` OK
* @response `401` `WebResp` Unauthorized
*/
export const putUserUpdateProfile = (
req: DomainProfileUpdateReq,
params: RequestParams = {},
) =>
request<
WebResp & {
data?: DomainUser;
}
>({
path: `/api/v1/user/profile`,
method: "PUT",
body: req,
type: ContentType.Json,
format: "json",
...params,
});

148
ui/src/api/UserRecord.ts Normal file
View File

@@ -0,0 +1,148 @@
/* 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 {
DomainChatInfo,
DomainCompletionInfo,
DomainListChatRecordResp,
DomainListCompletionRecordResp,
GetUserChatInfoParams,
GetUserCompletionInfoParams,
GetUserListChatRecordParams,
GetUserListCompletionRecordParams,
WebResp,
} from "./types";
/**
* @description 获取对话内容
*
* @tags User Record
* @name GetUserChatInfo
* @summary 获取对话内容
* @request GET:/api/v1/user/chat/info
* @response `200` `(WebResp & {
data?: DomainChatInfo,
})` OK
* @response `401` `string` Unauthorized
*/
export const getUserChatInfo = (
query: GetUserChatInfoParams,
params: RequestParams = {},
) =>
request<
WebResp & {
data?: DomainChatInfo;
}
>({
path: `/api/v1/user/chat/info`,
method: "GET",
query: query,
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description 获取用户对话记录
*
* @tags User Record
* @name GetUserListChatRecord
* @summary 获取用户对话记录
* @request GET:/api/v1/user/chat/record
* @response `200` `(WebResp & {
data?: DomainListChatRecordResp,
})` OK
* @response `401` `string` Unauthorized
*/
export const getUserListChatRecord = (
query: GetUserListChatRecordParams,
params: RequestParams = {},
) =>
request<
WebResp & {
data?: DomainListChatRecordResp;
}
>({
path: `/api/v1/user/chat/record`,
method: "GET",
query: query,
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description 获取补全内容
*
* @tags User Record
* @name GetUserCompletionInfo
* @summary 获取补全内容
* @request GET:/api/v1/user/completion/info
* @response `200` `(WebResp & {
data?: DomainCompletionInfo,
})` OK
* @response `401` `string` Unauthorized
*/
export const getUserCompletionInfo = (
query: GetUserCompletionInfoParams,
params: RequestParams = {},
) =>
request<
WebResp & {
data?: DomainCompletionInfo;
}
>({
path: `/api/v1/user/completion/info`,
method: "GET",
query: query,
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description 获取补全记录
*
* @tags User Record
* @name GetUserListCompletionRecord
* @summary 获取补全记录
* @request GET:/api/v1/user/completion/record
* @response `200` `(WebResp & {
data?: DomainListCompletionRecordResp,
})` OK
* @response `401` `string` Unauthorized
*/
export const getUserListCompletionRecord = (
query: GetUserListCompletionRecordParams,
params: RequestParams = {},
) =>
request<
WebResp & {
data?: DomainListCompletionRecordResp;
}
>({
path: `/api/v1/user/completion/record`,
method: "GET",
query: query,
type: ContentType.Json,
format: "json",
...params,
});

View File

@@ -61,7 +61,9 @@ export enum ContentType {
const redirectToLogin = () => {
const redirectAfterLogin = encodeURIComponent(location.href);
const search = `redirect=${redirectAfterLogin}`;
const pathname = "/login";
const pathname = location.pathname.startsWith("/user")
? "/user/login"
: "/login";
window.location.href = `${pathname}?${search}`;
};

View File

@@ -4,5 +4,8 @@ export * from './Dashboard'
export * from './Model'
export * from './OpenAiv1'
export * from './User'
export * from './UserDashboard'
export * from './UserManage'
export * from './UserRecord'
export * from './types'

View File

@@ -54,6 +54,11 @@ export enum ConstsModelProvider {
ModelProviderVolcengine = "Volcengine",
}
export enum ConstsLoginSource {
LoginSourcePlugin = "plugin",
LoginSourceBrowser = "browser",
}
export enum ConstsChatRole {
ChatRoleUser = "user",
ChatRoleAssistant = "assistant",
@@ -360,8 +365,10 @@ export interface DomainListUserResp {
export interface DomainLoginReq {
/** 密码 */
password?: string;
/** 会话Id */
/** 会话Id插件登录时必填 */
session_id?: string;
/** 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin */
source?: ConstsLoginSource;
/** 用户名 */
username?: string;
}
@@ -369,6 +376,8 @@ export interface DomainLoginReq {
export interface DomainLoginResp {
/** 重定向URL */
redirect_url?: string;
/** 用户信息 */
user?: DomainUser;
}
export interface DomainModel {
@@ -465,6 +474,17 @@ export interface DomainOAuthURLResp {
url?: string;
}
export interface DomainProfileUpdateReq {
/** 头像 */
avatar?: string;
/** 旧密码 */
old_password?: string;
/** 密码 */
password?: string;
/** 用户名 */
username?: string;
}
export interface DomainProviderModel {
/** 模型列表 */
models?: DomainModelBasic[];
@@ -727,13 +747,13 @@ export interface DomainUserStat {
}[];
/** 编程语言占比 */
program_language?: DomainCategoryPoint[];
/** 近90天总接受率 */
/** 总接受率 */
total_accepted_per?: number;
/** 近90天总对话任务数 */
/** 总对话任务数 */
total_chats?: number;
/** 近90天总补全任务数 */
/** 总补全任务数 */
total_completions?: number;
/** 近90天总代码行数 */
/** 总代码行数 */
total_lines_of_code?: number;
/** 工作模式占比 */
work_mode?: DomainCategoryPoint[];
@@ -935,6 +955,84 @@ export interface GetGetTokenUsageParams {
model_type: "llm" | "coder" | "embedding" | "audio" | "reranker";
}
export interface GetUserChatInfoParams {
/** 对话记录ID */
id: string;
}
export interface GetUserListChatRecordParams {
/** 作者 */
author?: string;
/** 是否接受筛选 */
is_accept?: boolean;
/** 语言 */
language?: string;
/** 下一页标识 */
next_token?: string;
/** 分页 */
page?: number;
/** 每页多少条记录 */
size?: number;
/** 工作模式 */
work_mode?: string;
}
export interface GetUserCompletionInfoParams {
/** 补全记录ID */
id: string;
}
export interface GetUserListCompletionRecordParams {
/** 作者 */
author?: string;
/** 是否接受筛选 */
is_accept?: boolean;
/** 语言 */
language?: string;
/** 下一页标识 */
next_token?: string;
/** 分页 */
page?: number;
/** 每页多少条记录 */
size?: number;
/** 工作模式 */
work_mode?: string;
}
export interface GetUserDashboardEventsParams {
/**
* 持续时间 (小时或天数)`
* @min 24
* @max 90
* @default 90
*/
duration?: number;
/**
* 精度: "hour", "day"
* @default "day"
*/
precision: "hour" | "day";
/** 用户ID可选参数 */
user_id?: string;
}
export interface GetUserDashboardStatParams {
/**
* 持续时间 (小时或天数)`
* @min 24
* @max 90
* @default 90
*/
duration?: number;
/**
* 精度: "hour", "day"
* @default "day"
*/
precision: "hour" | "day";
/** 用户ID可选参数 */
user_id?: string;
}
export interface DeleteDeleteUserParams {
/** 用户ID */
id: string;
@@ -972,4 +1070,6 @@ export interface GetUserOauthSignupOrInParams {
redirect_url?: string;
/** 会话ID */
session_id?: string;
/** 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin */
source?: "plugin" | "browser";
}

File diff suppressed because one or more lines are too long

View File

@@ -81,6 +81,13 @@ const USER_MENUS = [
icon: 'icon-buquanjilu',
show: true,
},
// {
// label: '设置',
// value: '/user/setting',
// pathname: '/user/setting',
// icon: 'icon-setting',
// show: true,
// },
];
const SidebarButton = styled(Button)(({ theme }) => ({

View File

@@ -27,7 +27,7 @@ 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';
import { DomainSetting, ConstsLoginSource } from '@/api/types';
// 样式化组件
const StyledContainer = styled(Container)(({ theme }) => ({

View File

@@ -1,6 +1,6 @@
import Avatar from '@/components/avatar';
import Card from '@/components/card';
import { getChatInfo } from '@/api/Billing';
import { getUserChatInfo } from '@/api/UserRecord';
import MarkDown from '@/components/markDown';
import { Ellipsis, Modal } from '@c-x/ui';
import { styled } from '@mui/material';
@@ -78,7 +78,7 @@ const ChatDetailModal = ({
const getChatDetailModal = () => {
if (!data) return;
getChatInfo({ id: data.id! }).then((res) => {
getUserChatInfo({ id: data.id! }).then((res) => {
setContent(res.contents || []);
});
};

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Table } from '@c-x/ui';
import { getListChatRecord } from '@/api/Billing';
import { getUserListChatRecord } from '@/api/UserRecord';
import dayjs from 'dayjs';
import Card from '@/components/card';
@@ -24,7 +24,7 @@ const Chat = () => {
>();
const fetchData = async () => {
setLoading(true);
const res = await getListChatRecord({
const res = await getUserListChatRecord({
page: page,
size: size,
});

View File

@@ -1,5 +1,5 @@
import Card from '@/components/card';
import { getCompletionInfo } from '@/api/Billing';
import { getUserCompletionInfo } from '@/api/UserRecord';
import { Modal } from '@c-x/ui';
import MonacoEditor from '@monaco-editor/react';
@@ -32,7 +32,7 @@ const ChatDetailModal = ({
const getChatDetailModal = () => {
if (!data) return;
getCompletionInfo({ id: data.id! }).then((res) => {
getUserCompletionInfo({ id: data.id! }).then((res) => {
// 先去除 <|im_start|> 和 <|im_end|> 及其间内容
const rawPrompt = removeImBlocks(res.prompt || '');
const content = res.content || '';

View File

@@ -1,7 +1,6 @@
import { useState, useEffect } from 'react';
import { DomainCompletionRecord, DomainUser } from '@/api/types';
import { getListCompletionRecord } from '@/api/Billing';
import { useRequest } from 'ahooks';
import { getUserListCompletionRecord } from '@/api/UserRecord';
import { Table } from '@c-x/ui';
import Card from '@/components/card';
import {
@@ -14,7 +13,6 @@ import {
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';
@@ -34,29 +32,19 @@ const Completion = () => {
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: 9999,
})
);
useEffect(() => {
setPage(1); // 筛选变化时重置页码
fetchData({
page: 1,
language: filterLang,
author: filterUser,
is_accept: filterAccept,
});
}, [filterUser, filterLang, filterAccept]);
}, [filterLang, filterAccept]);
const fetchData = async (params: {
page?: number;
@@ -67,11 +55,10 @@ const Completion = () => {
}) => {
setLoading(true);
const isAccept = params.is_accept || filterAccept;
const res = await getListCompletionRecord({
const res = await getUserListCompletionRecord({
page: params.page || page,
size: params.size || size,
language: params.language || filterLang,
author: params.author || filterUser,
is_accept:
isAccept === 'accepted'
? true

View File

@@ -1,16 +1,15 @@
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';
getUserDashboardEvents,
getUserDashboardHeatmap,
getUserDashboardStat,
} from '@/api/UserDashboard';
import { StyledHighlight } from '@/pages/dashboard/components/globalStatistic';
import { getRecent90DaysData, getRecent24HoursData } from '@/utils';
import { DomainUser } from '@/api/types';
@@ -33,42 +32,28 @@ const MemberStatistic = ({
precision: timeRange === '90d' ? 'day' : 'hour',
});
const { id } = useParams();
const { data: userEvents } = useRequest(
() =>
getUserEventsDashboard({
user_id: id || '',
getUserDashboardEvents({
precision: timeDuration.precision,
}),
{
refreshDeps: [id],
manual: false,
ready: !!id,
}
);
const { data: userStat } = useRequest(
() =>
getUserStatDashboard({
user_id: id || '',
getUserDashboardStat({
...timeDuration,
}),
{
refreshDeps: [id, timeDuration],
refreshDeps: [timeDuration],
manual: false,
ready: !!id,
}
);
const { data: userHeatmap } = useRequest(
() =>
getUserHeatmapDashboard({
user_id: id || '',
}),
{
refreshDeps: [id],
manual: false,
ready: !!id,
}
);
const { data: userHeatmap } = useRequest(getUserDashboardHeatmap, {
manual: false,
});
useEffect(() => {
setTimeDuration({

View File

@@ -1,22 +1,16 @@
import { useEffect, useMemo, useState } from 'react';
import { getListUser } from '@/api/User';
import { useState } from 'react';
import { Stack, MenuItem, Select } from '@mui/material';
import { getUserProfile } from '@/api/UserManage';
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 = {} } = useRequest(getUserProfile);
return (
<Stack gap={2} sx={{ height: '100%' }}>
<Stack direction='row' justifyContent='space-between' alignItems='center'>
@@ -32,7 +26,7 @@ const Dashboard = () => {
</Select>
</Stack>
<MemberStatistic memberData={memberData} timeRange={timeRange} />
<MemberStatistic memberData={userData as any} timeRange={timeRange} />
</Stack>
);
};

View File

@@ -28,7 +28,7 @@ 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';
import { DomainSetting, ConstsLoginSource } from '@/api/types';
// 样式化组件
const StyledContainer = styled(Container)(({ theme }) => ({
@@ -132,36 +132,28 @@ const UserLogin = () => {
}, []);
// 处理登录表单提交
const onSubmit = useCallback(
async (data: LoginFormData) => {
setLoading(true);
setError(null);
const onSubmit = useCallback(async (data: LoginFormData) => {
setLoading(true);
setError(null);
try {
const sessionId = searchParams.get('session_id');
if (!sessionId) {
message.error('缺少会话ID参数');
return;
}
try {
// 用户登录
const loginResult = await postLogin({
...data,
source: ConstsLoginSource.LoginSourceBrowser,
});
// 用户登录
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]
);
const redirectUrl = getRedirectUrl('user');
window.location.href = redirectUrl.href;
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : '登录失败,请重试';
setError(errorMessage);
console.error('登录失败:', err);
} finally {
setLoading(false);
}
}, []);
// 初始化背景动画
useEffect(() => {
@@ -266,10 +258,11 @@ const UserLogin = () => {
);
const onOauthLogin = (platform: 'dingtalk' | 'custom') => {
const redirectUrl = getRedirectUrl();
const redirectUrl = getRedirectUrl('user');
getUserOauthSignupOrIn({
platform,
redirect_url: redirectUrl.href,
source: ConstsLoginSource.LoginSourceBrowser,
}).then((res) => {
if (res.url) {
window.location.href = res.url;

View File

@@ -131,9 +131,11 @@ export const isValidUrl = (url: string) => {
return regex.test(url);
};
export const getRedirectUrl = () => {
export const getRedirectUrl = (source: 'user' | 'admin' = 'admin') => {
const searchParams = new URLSearchParams(location.search);
const redirect = searchParams.get('redirect') || '/dashboard';
const redirect =
searchParams.get('redirect') ||
`${source === 'admin' ? '' : '/user'}/dashboard`;
let redirectUrl: URL | null = null;
try {
redirectUrl = redirect ? new URL(decodeURIComponent(redirect)) : null;
@@ -145,7 +147,10 @@ export const getRedirectUrl = () => {
redirectUrl = isValidUrl(redirectUrl?.href || '')
? redirectUrl
: new URL('/dashboard', location.origin);
: new URL(
`${source === 'admin' ? '' : '/user'}/dashboard`,
location.origin
);
return redirectUrl as URL;
};