feat(web): app page ui upgrade
This commit is contained in:
@@ -2,10 +2,10 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-05 10:45:08
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-10 17:59:37
|
||||
* @Last Modified time: 2026-03-04 10:41:35
|
||||
*/
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { Space, List, Flex, Tooltip } from 'antd';
|
||||
import { List, Flex, Tooltip, Form } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx'
|
||||
|
||||
@@ -31,7 +31,7 @@ interface SkillModalProps {
|
||||
*
|
||||
* A modal dialog for selecting skills from a searchable list.
|
||||
* Features:
|
||||
* - Search functionality to filter skills by search
|
||||
* - Search functionality to filter skills by keywords
|
||||
* - Grid layout displaying skill cards with icons and descriptions
|
||||
* - Multi-select capability with visual feedback
|
||||
* - Excludes already selected skills from the list
|
||||
@@ -49,17 +49,19 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [list, setList] = useState<Skill[]>([])
|
||||
const [filterList, setFilterList] = useState<Skill[]>([])
|
||||
const [query, setQuery] = useState<{search?: string}>({})
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||
const [selectedRows, setSelectedRows] = useState<Skill[]>([])
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const query = Form.useWatch([], form)
|
||||
|
||||
/**
|
||||
* Closes the modal and resets all state
|
||||
* Clears search query, selected IDs, and selected rows
|
||||
*/
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
setQuery({})
|
||||
form.resetFields()
|
||||
setSelectedIds([])
|
||||
setSelectedRows([])
|
||||
};
|
||||
@@ -70,7 +72,7 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
|
||||
*/
|
||||
const handleOpen = () => {
|
||||
setVisible(true);
|
||||
setQuery({})
|
||||
form.resetFields()
|
||||
setSelectedIds([])
|
||||
setSelectedRows([])
|
||||
};
|
||||
@@ -82,7 +84,7 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
|
||||
if (visible) {
|
||||
getList()
|
||||
}
|
||||
}, [query.search, visible])
|
||||
}, [query?.search, visible])
|
||||
|
||||
/**
|
||||
* Fetches the skill list from API with current search parameters
|
||||
@@ -96,6 +98,8 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
|
||||
.then(res => {
|
||||
const response = res as { items: Skill[] }
|
||||
setList(response.items || [])
|
||||
setSelectedIds([])
|
||||
setSelectedRows([])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -117,17 +121,6 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
|
||||
handleClose
|
||||
}));
|
||||
|
||||
/**
|
||||
* Handles search input changes and resets selection
|
||||
* Clears current selections when search query changes
|
||||
* @param value - Search keyword
|
||||
*/
|
||||
const handleSearch = (value?: string) => {
|
||||
setQuery({search: value})
|
||||
setSelectedIds([])
|
||||
setSelectedRows([])
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles skill selection state
|
||||
* Adds skill to selection if not selected, removes if already selected
|
||||
@@ -154,7 +147,7 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
|
||||
if (list.length && selectedList.length) {
|
||||
const unSelectedList = list.filter(item => selectedList.findIndex(vo => vo.id === item.id) < 0)
|
||||
setFilterList([...unSelectedList])
|
||||
} else if (list.length) {
|
||||
} else {
|
||||
setFilterList([...list])
|
||||
}
|
||||
}, [list, selectedList])
|
||||
@@ -169,13 +162,16 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
|
||||
onOk={handleSave}
|
||||
width={1000}
|
||||
>
|
||||
<Space size={24} direction="vertical" className="rb:w-full">
|
||||
<Flex gap={24} vertical>
|
||||
{/* Search input for filtering skills */}
|
||||
<SearchInput
|
||||
placeholder={t('skills.searchPlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Form form={form}>
|
||||
<Form.Item name="search" noStyle>
|
||||
<SearchInput
|
||||
placeholder={t('skills.searchPlaceholder')}
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{/* Display empty state or skill grid */}
|
||||
{filterList.length === 0
|
||||
? <Empty />
|
||||
@@ -191,9 +187,9 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
|
||||
})} onClick={() => handleSelect(item)}>
|
||||
<Flex>
|
||||
{/* Skill avatar showing first letter of name */}
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
<Flex align="center" justify="center" className="rb:size-12 rb:rounded-lg rb:mr-3.25! rb:bg-[#155eef] rb:text-[28px] rb:text-white">
|
||||
{item.name[0]}
|
||||
</div>
|
||||
</Flex>
|
||||
{/* Skill name and description */}
|
||||
<div className="rb:flex-1 rb:max-w-[calc(100%-60px)]">
|
||||
<div className="rb:font-medium rb:wrap-break-word rb:line-clamp-1">{item.name}</div>
|
||||
@@ -207,7 +203,7 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
|
||||
)}
|
||||
/>
|
||||
}
|
||||
</Space>
|
||||
</Flex>
|
||||
</RbModal>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-05 10:43:03
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 11:10:01
|
||||
* @Last Modified time: 2026-02-25 15:36:14
|
||||
*/
|
||||
import { type FC, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Space, Button, Form, Flex, Tooltip, Checkbox } from 'antd'
|
||||
import { CloseOutlined, CheckCircleFilled } from '@ant-design/icons'
|
||||
import { CheckCircleFilled } from '@ant-design/icons'
|
||||
|
||||
import type {
|
||||
SkillConfigForm,
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
} from './types'
|
||||
import Empty from '@/components/Empty'
|
||||
import SkillListModal from './SkillListModal'
|
||||
import Card from '../Card'
|
||||
import RbAlert from '@/components/RbAlert'
|
||||
import type { Skill } from '@/views/Skills/types'
|
||||
|
||||
@@ -86,20 +85,19 @@ const SkillsItem: FC<SkillsItemProps> = ({
|
||||
}, [allSkills])
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={title}
|
||||
extra={
|
||||
<Space>
|
||||
<div>
|
||||
<Flex align="center" justify="space-between" className="rb:mb-2!">
|
||||
<div className="rb:text-[#212332] rb:font-medium rb:leading-4.5 rb:px-1">{title}</div>
|
||||
|
||||
<Space size={16}>
|
||||
{/* "Allow all skills" checkbox - only shown if supportAll is true */}
|
||||
{supportAll && <Form.Item name={[...parentName, 'all_skills']} valuePropName="checked" noStyle>
|
||||
<Checkbox>{t('application.allSkill')}</Checkbox>
|
||||
<Checkbox className="rb:text-[12px]!">{t('application.allSkill')}</Checkbox>
|
||||
</Form.Item>}
|
||||
{/* Add skill button - disabled when all skills are enabled */}
|
||||
<Button disabled={allSkills} style={{ padding: '0 8px', height: '24px' }} onClick={handleAddSkill}>+ {t('application.addSkill')}</Button>
|
||||
<Button disabled={allSkills} type="link" className="rb:h-4! rb:p-0! rb:font-medium! rb:text-[12px]! rb:text-[#212332]" onClick={handleAddSkill}>+ {t('application.addSkill')}</Button>
|
||||
</Space>
|
||||
}
|
||||
variant="borderL"
|
||||
>
|
||||
</Flex>
|
||||
{/* Show alert when all skills enabled, otherwise show skill list */}
|
||||
{allSkills
|
||||
? <RbAlert color="green" icon={<CheckCircleFilled />}>{t('application.allSkillIntro')}</RbAlert>
|
||||
@@ -111,39 +109,29 @@ const SkillsItem: FC<SkillsItemProps> = ({
|
||||
<Empty size={88} subTitle={emptyTitle} />
|
||||
) : (
|
||||
/* Render list of configured skills */
|
||||
<Space direction="vertical" size={12} className="rb:w-full">
|
||||
<Flex vertical gap={12}>
|
||||
{fields.map((field) => {
|
||||
const skill = form.getFieldValue([...parentName, 'skill_ids', field.name])
|
||||
return (
|
||||
/* Individual skill card */
|
||||
<div key={field.key} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<Flex key={field.key} align="center" justify="space-between" className="rb:p-3! rb:bg-[#FFFFFF] rb-border rb:rounded-lg">
|
||||
<Flex className="rb:flex-1 rb:max-w-[calc(100%-186px)]!">
|
||||
{/* Skill icon or fallback initial */}
|
||||
{skill.icon
|
||||
? <img src={skill.icon} className="rb:mr-3.25 rb:size-12 rb:rounded-lg" />
|
||||
: <div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{skill.name?.[0]}
|
||||
</div>
|
||||
}
|
||||
{/* Skill name and description */}
|
||||
<div className="rb:flex-1 rb:max-w-[calc(100%-60px)]">
|
||||
<div className="rb:font-medium rb:wrap-break-word rb:line-clamp-1">{skill.name}</div>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:leading-5 rb:wrap-break-word rb:line-clamp-1">{skill.name}</div>
|
||||
<Tooltip title={skill.description}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1">{skill.description}</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:mt-1 rb:wrap-break-word rb:line-clamp-1">{skill.description}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Flex>
|
||||
<Space size={16} align="center">
|
||||
{/* Remove skill button */}
|
||||
<CloseOutlined
|
||||
className="rb:cursor-pointer rb:text-[#5B6167] hover:rb:text-[#155EEF]"
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
<div
|
||||
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => remove(field.name)}
|
||||
></div>
|
||||
</Flex>
|
||||
)
|
||||
})}
|
||||
</Space>
|
||||
</Flex>
|
||||
)
|
||||
)}
|
||||
</Form.List>
|
||||
@@ -155,7 +143,7 @@ const SkillsItem: FC<SkillsItemProps> = ({
|
||||
selectedList={form.getFieldValue([...parentName, 'skill_ids']) || []}
|
||||
refresh={refresh}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default SkillsItem
|
||||
@@ -1,17 +1,18 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-05 10:42:56
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-05 10:42:56
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-26 10:18:56
|
||||
*/
|
||||
import { useEffect, type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Space, Button, Switch, Form, Flex } from 'antd'
|
||||
import { Space, Switch, Form, Flex } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import type {
|
||||
SkillConfigForm,
|
||||
} from './types'
|
||||
import Card from '../Card'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import SkillsItem from './SkillsItem'
|
||||
import { getSkillList } from '@/api/skill'
|
||||
import type { Skill } from '@/views/Skills/types'
|
||||
@@ -94,15 +95,15 @@ const SkillList: FC<{value?: SkillConfigForm; onChange?: (config: SkillConfigFor
|
||||
|
||||
|
||||
return (
|
||||
<Card
|
||||
<RbCard
|
||||
title={<>
|
||||
{t('application.skill')}
|
||||
<span className="rb:font-regular rb:text-[12px] rb:text-[#5B6167]"> ({t('application.skillTitle')})</span>
|
||||
<div className="rb:font-[MiSans-Bold] rb:font-bold">{t('application.skill')}</div>
|
||||
<div className="rb:font-regular! rb:text-[12px] rb:text-[#5B6167]"> {t('application.skillTitle')}</div>
|
||||
</>}
|
||||
extra={
|
||||
<Space>
|
||||
{/* Help button for skill configuration guidance */}
|
||||
<Button style={{ padding: '0 8px', height: '24px' }}>{t('application.skillHelp')}</Button>
|
||||
{/* <Button style={{ padding: '0 8px', height: '24px' }}>{t('application.skillHelp')}</Button> */}
|
||||
{/* Toggle switch to enable/disable skill functionality */}
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
@@ -113,9 +114,28 @@ const SkillList: FC<{value?: SkillConfigForm; onChange?: (config: SkillConfigFor
|
||||
</Form.Item>
|
||||
</Space>
|
||||
}
|
||||
headerType="borderless"
|
||||
headerClassName={clsx("rb:py-[16px]! rb:leading-[22px]! rb:font-regular", {
|
||||
'rb:h-[76px]! rb:py-[16px]!': !skillConfig?.enabled,
|
||||
'rb:h-[68px]! rb:pb-2!': skillConfig?.enabled,
|
||||
})}
|
||||
>
|
||||
{/* Render skill configuration UI only when enabled */}
|
||||
{skillConfig?.enabled && <Flex vertical gap={12}>
|
||||
{skillConfig?.enabled && <Flex vertical gap={8} className="rb:bg-[#FAFAFA] rb:rounded-xl rb:pt-2.5! rb:pb-3! rb:px-3!">
|
||||
<div className="rb:text-[#212332] rb:font-medium rb:leading-4.5 rb:px-1">{t('application.executeProcessPreview')}</div>
|
||||
<Flex align="center" justify="space-between" gap={14} className="rb:text-[12px] rb:bg-[#FFFFFF]! rb:rounded-lg rb-border rb:py-2.5! rb:pl-4! rb:pr-3.25! rb:mb-2!">
|
||||
{/* Render each step in the process flow with numbered badges */}
|
||||
{processObj.map((key, index) => (<>
|
||||
<Flex align="center" gap={8}>
|
||||
{/* Step number badge */}
|
||||
<Flex align="center" justify="center" className="rb:size-4 rb:rounded-full rb:bg-[#171719] rb:text-white rb:font-medium">{index + 1}</Flex>
|
||||
{/* Step label */}
|
||||
<span className="rb:inline-block rb:max-w-16">{t(`application.${key}`)}</span>
|
||||
</Flex>
|
||||
{/* Arrow separator between steps (except after last step) */}
|
||||
{index !== processObj.length - 1 && <div className="rb:w-10 rb:h-4.5 rb:bg-cover rb:bg-[url('@/assets/images/application/arrow_right.svg')]"></div>}
|
||||
</>))}
|
||||
</Flex>
|
||||
{/* Dynamic skill binding configuration section */}
|
||||
<Form.Item noStyle>
|
||||
<SkillsItem
|
||||
@@ -125,27 +145,8 @@ const SkillList: FC<{value?: SkillConfigForm; onChange?: (config: SkillConfigFor
|
||||
emptyTitle={t('application.dynamicBindingSkill_empty')}
|
||||
/>
|
||||
</Form.Item>
|
||||
{/* Execution process preview card showing workflow steps */}
|
||||
<Card
|
||||
title={t('application.executeProcessPreview')}
|
||||
variant="borderL"
|
||||
>
|
||||
<Flex align="center" gap={8} className="rb:text-[12px]">
|
||||
{/* Render each step in the process flow with numbered badges */}
|
||||
{processObj.map((key, index) => (<>
|
||||
<Flex align="center" gap={8} className="rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:bg-white rb:p-2!">
|
||||
{/* Step number badge */}
|
||||
<div className="rb:size-4 rb:rounded-full rb:bg-[#155EEF] rb:text-white rb:flex rb:items-center rb:justify-center rb:font-medium">{index + 1}</div>
|
||||
{/* Step label */}
|
||||
<span>{t(`application.${key}`)}</span>
|
||||
</Flex>
|
||||
{/* Arrow separator between steps (except after last step) */}
|
||||
{index !== processObj.length - 1 && <div className="rb:text-[#8C9196]">→</div>}
|
||||
</>))}
|
||||
</Flex>
|
||||
</Card>
|
||||
</Flex>}
|
||||
</Card>
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
export default SkillList
|
||||
Reference in New Issue
Block a user