Merge pull request #975 from SuanmoSuanyangTechnology/feature/space_zy

feat(web): support switch space
This commit is contained in:
yingzhao
2026-04-22 18:51:07 +08:00
committed by GitHub
8 changed files with 194 additions and 19 deletions

View File

@@ -9,8 +9,9 @@ import type { SpaceModalData } from '@/views/SpaceManagement/types'
import type { SpaceConfigData } from '@/views/SpaceConfig/types'
// Workspace list
export const getWorkspacesUrl = '/workspaces'
export const getWorkspaces = (data?: { include_current?: boolean }) => {
return request.get('/workspaces', data)
return request.get(getWorkspacesUrl, data)
}
// Create workspace
export const createWorkspace = (values: SpaceModalData) => {

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>退出</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="工作台-记忆看板-3" transform="translate(-22, -855)" stroke="#171719" stroke-width="1.2">
<g id="退出" transform="translate(0, 791)">
<g id="返回空间" transform="translate(12, 53)">
<g id="退出" transform="translate(18, 19) scale(-1, 1) translate(-18, -19)translate(10, 11)">
<g id="编组-7" transform="translate(2.5, 2)">
<path d="M5,12 L1,12 C0.44771525,12 0,11.5522847 0,11 L0,1 C0,0.44771525 0.44771525,1.11022302e-16 1,0 L5,0 L5,0" id="路径"></path>
<line x1="11" y1="6" x2="3" y2="6" id="路径-6"></line>
<polyline id="路径" points="8 3 11 6 8 9"></polyline>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>切换</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="工作台-记忆看板-3" transform="translate(-22, -813)" stroke="#171719" stroke-width="1.2">
<g id="退出" transform="translate(0, 791)">
<g id="返回空间备份" transform="translate(12, 11)">
<g id="切换" transform="translate(10, 11)">
<g id="编组-33" transform="translate(1.5, 3.5)">
<path d="M6.18518092,1.69615364 L4.33333333,0 L8.66666667,0 C11.0599006,0 13,2.0118047 13,4.49349156 C13,5.84177845 12.4273429,7.05137071 11.5204839,7.875" id="路径"></path>
<path d="M1.85184759,2.82115364 L0,1.125 L4.33333333,1.125 C6.72656725,1.125 8.66666667,3.1368047 8.66666667,5.61849156 C8.66666667,6.96677845 8.09400958,8.17637071 7.18715055,9" id="路径" transform="translate(4.3333, 5.0625) scale(-1, -1) translate(-4.3333, -5.0625)"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,114 @@
/*
* @Author: ZhaoYing
* @Date: 2026-04-22 18:50:14
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-22 18:50:14
*/
/**
* SwitchSpaceModal Component
*
* A modal for switching the current workspace.
* Displays a dropdown to select a workspace and reloads the page upon confirmation.
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, App, Space } from 'antd';
import { useTranslation } from 'react-i18next';
import RbModal from '@/components/RbModal'
import { switchWorkspace, getWorkspacesUrl } from '@/api/workspaces'
import CustomSelect from '@/components/CustomSelect';
import Tag from '@/components/Tag'
import { useUser } from '@/store/user';
const FormItem = Form.Item;
export interface SwitchSpaceModalRef {
handleOpen: () => void;
}
const SwitchSpaceModal = forwardRef<SwitchSpaceModalRef>((_props, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<{ space_id: string }>();
const [loading, setLoading] = useState(false)
const { user } = useUser()
/** Close modal and reset form */
const handleClose = () => {
setVisible(false);
form.resetFields();
setLoading(false)
};
/** Open modal */
const handleOpen = () => {
form.resetFields();
setVisible(true);
form.setFieldsValue({ space_id: user?.current_workspace_id })
};
/** Handle save/next button click - proceed to next step or submit email change */
const handleSave = () => {
form
.validateFields()
.then((values) => {
if (user?.current_workspace_id === values.space_id) {
handleClose()
return
}
setLoading(true)
switchWorkspace(values.space_id)
.then(res => {
if (res) {
message.success(t('common.operateSuccess'));
localStorage.removeItem('user')
window.location.reload()
}
})
.finally(() => setLoading(false))
})
.catch((err) => {
console.log('err', err)
});
}
/** Expose methods to parent component */
useImperativeHandle(ref, () => ({
handleOpen,
}));
return (
<RbModal
title={t('common.switchSpace')}
open={visible}
onCancel={handleClose}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
>
<FormItem
name="space_id"
label={t('space.spaceName')}
rules={[
{ required: true, message: t('common.pleaseSelect') },
]}
>
<CustomSelect
url={getWorkspacesUrl}
hasAll={false}
format={(list) => list.map(item => ({
value: item.id,
label: <Space>{item.name}<Tag color={item.storage_type === 'rag' ? 'processing' : 'warning'}>{t(`space.${item.storage_type || 'neo4j'}`)}</Tag></Space>
}))}
/>
</FormItem>
</Form>
</RbModal>
);
});
export default SwitchSpaceModal;

View File

@@ -2,6 +2,10 @@
/* border-right: 1px solid #EAECEE; */
max-height: 100vh;
}
.sider :global(.ant-layout-sider-children) {
display: flex;
flex-direction: column;
}
.title {
height: 64px;
padding: 24px 10px 12px 12px;

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-02 15:25:31
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-16 17:35:38
* @Last Modified time: 2026-04-21 17:56:09
*/
/**
* SiderMenu Component
@@ -19,7 +19,7 @@
*/
import { useState, useEffect, useRef, type FC } from 'react';
import { Menu as AntMenu, Layout, Flex } from 'antd';
import { Menu as AntMenu, Layout, Flex, Divider } from 'antd';
import { UserOutlined } from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { useNavigate, useLocation } from 'react-router-dom';
@@ -32,7 +32,8 @@ import logo from '@/assets/images/logo.png'
import { useUser } from '@/store/user';
import { getTenantSubscription } from '@/api/user';
import { useI18n } from '@/store/locale'
import SubscriptionDetailModal, { type SubscriptionDetailModalRef } from './SubscriptionDetailModal'
import SubscriptionDetailModal, { type SubscriptionDetailModalRef } from './SubscriptionDetailModal';
import SwitchSpaceModal, { type SwitchSpaceModalRef } from './SwitchSpaceModal';
// Import SVG files
// space
@@ -174,6 +175,7 @@ const Menu: FC<{
const [menus, setMenus] = useState<MenuItem[]>([])
const { user, storageType } = useUser()
const subscriptionDetailRef = useRef<SubscriptionDetailModalRef>(null)
const switchSpaceModalRef = useRef<SwitchSpaceModalRef>(null)
/** Filter menus based on user role and source */
useEffect(() => {
@@ -346,6 +348,9 @@ const Menu: FC<{
const handleViewDetail = () => {
subscriptionDetailRef.current?.handleOpen(subscription)
}
const handleSwitchSpace = () => {
switchSpaceModalRef.current?.handleOpen()
}
return (
<Sider
@@ -391,27 +396,36 @@ const Menu: FC<{
items={menuItems}
inlineCollapsed={collapsed}
inlineIndent={10}
className={clsx("rb:overflow-y-auto", {
'rb:max-h-[calc(100vh-136px)]': user?.is_superuser && source === 'space',
'rb:max-h-[calc(100vh-76px)]': !(user?.is_superuser && source === 'space') && !(source === 'manage' && subscription && !collapsed),
'rb:max-h-[calc(100vh-228px)]': source === 'manage' && subscription && !collapsed,
})}
className="rb:overflow-y-auto rb:flex-1!"
/>
{/* Return to space button for superusers */}
{user?.is_superuser && source === 'space' &&
<Flex
gap={8}
align="center"
justify="start"
onClick={goToSpace}
className="rb-border-t rb:pt-5! rb:pb-2.5! rb:absolute rb:bottom-2.5 rb:right-5 rb:left-5 rb:text-[13px] rb:text-[#5B6167] rb:hover:text-[#212332] rb:leading-4.5 rb:font-regular rb:text-center rb:mt-2.25 rb:cursor-pointer"
>
<div className="rb:cursor-pointer rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/logout_grey.svg')]"></div>
{collapsed ? null : t('common.returnToSpace')}
<Flex gap={4} vertical className="rb:my-3! rb:mx-3!">
<Divider className="rb:mb-2.5! rb:mt-0! rb:border-[#DFE4ED]! rb:mx-2! rb:min-w-[calc(100%-20px)]! rb:w-[calc(100%-20px)]!" />
<Flex
gap={8}
align="center"
justify="start"
onClick={handleSwitchSpace}
className="rb:p-2.5! rb:text-[13px] rb:hover:bg-[rgba(223,228,237,0.5)] rb:rounded-lg rb:leading-3.5 rb:font-regular rb:text-center rb:cursor-pointer"
>
<div className="rb:cursor-pointer rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/switch.svg')]"></div>
{collapsed ? null : t('common.switchSpace')}
</Flex>
<Flex
gap={8}
align="center"
justify="start"
onClick={goToSpace}
className="rb:p-2.5! rb:text-[13px] rb:hover:bg-[rgba(223,228,237,0.5)] rb:rounded-lg rb:leading-3.5 rb:font-regular rb:text-center rb:cursor-pointer"
>
<div className="rb:cursor-pointer rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/return.svg')]"></div>
{collapsed ? null : t('common.returnToSpace')}
</Flex>
</Flex>
}
{source === 'manage' && subscription && !collapsed &&
<div className="rb:absolute rb:bottom-3 rb:left-3 rb:right-3 rb:py-3 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/package_bg.png')] rb:overflow-hidden rb:rounded-xl">
<div className="rb:mb-3 rb:ml-3 rb:mr-3 rb:py-3 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/package_bg.png')] rb:overflow-hidden rb:rounded-xl">
<div className="rb:h-4.5 rb:flex-1 rb:truncate rb:px-3 rb:text-[13px] rb:font-medium rb:leading-4.5">{subscription.package_plan?.[getKeyWithLanguage('name')]}</div>
<div className="rb:grid rb:grid-cols-4 rb:mt-4">
@@ -434,6 +448,9 @@ const Menu: FC<{
<SubscriptionDetailModal
ref={subscriptionDetailRef}
/>
<SwitchSpaceModal
ref={switchSpaceModalRef}
/>
</Sider>
);
};

View File

@@ -474,6 +474,7 @@ export const en = {
view: 'View',
updated_at: 'Updated At',
callbackUrlInvalid: 'Please enter a valid URL',
switchSpace: 'Switch Space',
},
model: {
searchPlaceholder: 'search model…',

View File

@@ -1153,6 +1153,7 @@ export const zh = {
view: '查看',
updated_at: '更新时间',
callbackUrlInvalid: '请输入有效的 URL',
switchSpace: '切换空间',
},
model: {
searchPlaceholder: '搜索模型…',