diff --git a/backend/apps/datasource/crud/datasource.py b/backend/apps/datasource/crud/datasource.py index 868e979e..6c722244 100644 --- a/backend/apps/datasource/crud/datasource.py +++ b/backend/apps/datasource/crud/datasource.py @@ -400,7 +400,9 @@ def updateNum(session: SessionDep, ds: CoreDatasource): def get_table_obj_by_ds(session: SessionDep, current_user: CurrentUser, ds: CoreDatasource) -> List[TableAndFields]: _list: List = [] - tables = session.query(CoreTable).filter(CoreTable.ds_id == ds.id).all() + tables = session.query(CoreTable).filter( + and_(CoreTable.ds_id == ds.id, CoreTable.checked == True) + ).all() conf = DatasourceConf(**json.loads(aes_decrypt(ds.configuration))) if ds.type != "excel" else get_engine_config() schema = conf.dbSchema if conf.dbSchema is not None and conf.dbSchema != "" else conf.database diff --git a/backend/apps/db/db.py b/backend/apps/db/db.py index 5add5f81..24114a11 100644 --- a/backend/apps/db/db.py +++ b/backend/apps/db/db.py @@ -670,7 +670,7 @@ def check_sql_read(sql: str, ds: CoreDatasource | AssistantOutDsSchema): write_types = ( exp.Insert, exp.Update, exp.Delete, exp.Create, exp.Drop, exp.Alter, - exp.Merge, exp.Command + exp.Merge, exp.Command, exp.Copy ) for stmt in statements: @@ -688,6 +688,7 @@ def check_sql_read(sql: str, ds: CoreDatasource | AssistantOutDsSchema): def checkParams(extraParams: str, illegalParams: List[str]): kvs = extraParams.split('&') for kv in kvs: - k, v = kv.split('=') - if k in illegalParams: - raise HTTPException(status_code=500, detail=f'Illegal Parameter: {k}') + if kv and '=' in kv: + k, v = kv.split('=') + if k in illegalParams: + raise HTTPException(status_code=500, detail=f'Illegal Parameter: {k}') diff --git a/backend/apps/system/api/user.py b/backend/apps/system/api/user.py index 41be149d..6d3f4946 100644 --- a/backend/apps/system/api/user.py +++ b/backend/apps/system/api/user.py @@ -180,7 +180,8 @@ async def create(session: SessionDep, creator: UserCreator, trans: Trans): raise Exception(trans('i18n_exist', msg = f"{trans('i18n_user.email')} [{creator.email}]")) if not check_email_format(creator.email): raise Exception(trans('i18n_format_invalid', key = f"{trans('i18n_user.email')} [{creator.email}]")) - data = creator.model_dump(exclude_unset=True) + #data = creator.model_dump(exclude_unset=True) + data = creator.model_dump() user_model = UserModel.model_validate(data) #user_model.create_time = get_timestamp() user_model.language = "zh-CN" diff --git a/backend/apps/system/crud/assistant.py b/backend/apps/system/crud/assistant.py index 56bfcec6..f4d6cbc5 100644 --- a/backend/apps/system/crud/assistant.py +++ b/backend/apps/system/crud/assistant.py @@ -21,6 +21,7 @@ from common.core.sqlbot_cache import cache from common.utils.aes_crypto import simple_aes_decrypt from common.utils.utils import SQLBotLogUtil, equals_ignore_case, get_domain_list, string_to_numeric_hash from common.core.deps import Trans +from common.core.response_middleware import ResponseMiddleware @cache(namespace=CacheNamespace.EMBEDDED_INFO, cacheName=CacheName.ASSISTANT_INFO, keyExpression="assistant_id") @@ -87,13 +88,20 @@ def init_dynamic_cors(app: FastAPI): seen.add(domain) unique_domains.append(domain) cors_middleware = None + response_middleware = None for middleware in app.user_middleware: - if middleware.cls == CORSMiddleware: + if not cors_middleware and middleware.cls == CORSMiddleware: cors_middleware = middleware + if not response_middleware and middleware.cls == ResponseMiddleware: + response_middleware = middleware + if cors_middleware and response_middleware: break + + updated_origins = list(set(settings.all_cors_origins + unique_domains)) if cors_middleware: - updated_origins = list(set(settings.all_cors_origins + unique_domains)) cors_middleware.kwargs['allow_origins'] = updated_origins + if response_middleware: + response_middleware.kwargs['allow_origins'] = updated_origins except Exception as e: return False, e diff --git a/backend/apps/system/crud/system_variable.py b/backend/apps/system/crud/system_variable.py index 50ebe490..0ead75d2 100644 --- a/backend/apps/system/crud/system_variable.py +++ b/backend/apps/system/crud/system_variable.py @@ -1,8 +1,8 @@ # Author: Junjun # Date: 2026/1/26 import datetime - from typing import List + from fastapi import HTTPException from sqlalchemy import and_ from sqlmodel import select @@ -37,11 +37,12 @@ def delete(session: SessionDep, ids: List[int]): def list_all(session: SessionDep, trans: Trans, variable: SystemVariable): if variable.name is None: - records = session.query(SystemVariable).order_by(SystemVariable.type.desc()).all() + records = session.query(SystemVariable).order_by(SystemVariable.type.desc(), + SystemVariable.name.asc()).all() else: records = session.query(SystemVariable).filter( - and_(SystemVariable.name.like(f'%{variable.name}%'), SystemVariable.type != 'system')).order_by( - SystemVariable.type.desc()).all() + and_(SystemVariable.name.ilike(f'%{variable.name}%'), SystemVariable.type != 'system')).order_by( + SystemVariable.type.desc(), SystemVariable.name.asc()).all() res = [] for r in records: @@ -58,11 +59,11 @@ async def list_page(session: SessionDep, trans: Trans, pageNum: int, pageSize: i filters = {} if variable.name is None: - stmt = select(SystemVariable).order_by(SystemVariable.type.desc()) + stmt = select(SystemVariable).order_by(SystemVariable.type.desc(), SystemVariable.name.asc()) else: stmt = select(SystemVariable).where( - and_(SystemVariable.name.like(f'%{variable.name}%'), SystemVariable.type != 'system')).order_by( - SystemVariable.type.desc()) + and_(SystemVariable.name.ilike(f'%{variable.name}%'), SystemVariable.type != 'system')).order_by( + SystemVariable.type.desc(), SystemVariable.name.asc()) variable_page = await paginator.get_paginated_response( stmt=stmt, @@ -92,7 +93,7 @@ def checkName(session: SessionDep, trans: Trans, variable: SystemVariable): raise HTTPException(status_code=500, detail=trans('i18n_variable.name_exist')) -def checkValue(session: SessionDep, trans: Trans, values:List): +def checkValue(session: SessionDep, trans: Trans, values: List): # values: [{"variableId":1,"variableValues":["a","b"]}] - pass \ No newline at end of file + pass diff --git a/backend/apps/system/schemas/system_schema.py b/backend/apps/system/schemas/system_schema.py index 5bc6f72e..5be16668 100644 --- a/backend/apps/system/schemas/system_schema.py +++ b/backend/apps/system/schemas/system_schema.py @@ -58,7 +58,7 @@ class UserCreator(BaseUser): status: int = Field(default=1, description=f"{PLACEHOLDER_PREFIX}status") origin: Optional[int] = Field(default=0, description=f"{PLACEHOLDER_PREFIX}origin") oid_list: Optional[list[int]] = Field(default=None, description=f"{PLACEHOLDER_PREFIX}oid") - system_variables: Optional[List] = Field(default=None) + system_variables: Optional[List] = Field(default=[]) """ @field_validator("email") def validate_email(cls, lang: str) -> str: diff --git a/backend/apps/terminology/api/terminology.py b/backend/apps/terminology/api/terminology.py index 7240b278..b74cb19b 100644 --- a/backend/apps/terminology/api/terminology.py +++ b/backend/apps/terminology/api/terminology.py @@ -165,6 +165,7 @@ session_maker = scoped_session(sessionmaker(bind=engine, class_=Session)) @router.post("/uploadExcel", summary=f"{PLACEHOLDER_PREFIX}upload_term") @system_log(LogConfig(operation_type=OperationType.IMPORT, module=OperationModules.TERMINOLOGY)) +@require_permissions(permission=SqlbotPermission(role=['ws_admin'])) async def upload_excel(trans: Trans, current_user: CurrentUser, file: UploadFile = File(...)): ALLOWED_EXTENSIONS = {"xlsx", "xls"} if not file.filename.lower().endswith(tuple(ALLOWED_EXTENSIONS)): diff --git a/backend/common/core/response_middleware.py b/backend/common/core/response_middleware.py index dfd9f1dc..c2eeb43f 100644 --- a/backend/common/core/response_middleware.py +++ b/backend/common/core/response_middleware.py @@ -1,5 +1,6 @@ import json +from redis import typing from starlette.exceptions import HTTPException from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request @@ -11,6 +12,7 @@ from common.utils.utils import SQLBotLogUtil class ResponseMiddleware(BaseHTTPMiddleware): def __init__(self, app): + self.allow_origins = ["'self'"] super().__init__(app) async def dispatch(self, request, call_next): @@ -76,7 +78,13 @@ class ResponseMiddleware(BaseHTTPMiddleware): if k.lower() not in ("content-length", "content-type") } ) - + content_type = response.headers.get("content-type", "") + static_content_types = ["text/html", "javascript", "typescript", "css"] + if any(ct in content_type for ct in static_content_types): + if self.allow_origins: + frame_ancestors_value = " ".join(self.allow_origins) + response.headers["Content-Security-Policy"] = f"frame-ancestors {frame_ancestors_value};" + return response diff --git a/frontend/package.json b/frontend/package.json index e7d83cee..14d62376 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -64,7 +64,7 @@ "eslint-plugin-prettier": "^5.4.1", "eslint-plugin-vue": "^10.2.0", "globals": "^16.2.0", - "less": "^4.3.0", + "less": "4.4.2", "pinia": "^3.0.2", "prettier": "^3.5.3", "typescript": "~5.7.2", @@ -76,4 +76,4 @@ "vite-svg-loader": "^5.1.0", "vue-tsc": "^2.2.8" } -} +} \ No newline at end of file diff --git a/frontend/src/assets/svg/field_text.svg b/frontend/src/assets/svg/field_text.svg new file mode 100644 index 00000000..16fa7781 --- /dev/null +++ b/frontend/src/assets/svg/field_text.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/svg/field_time.svg b/frontend/src/assets/svg/field_time.svg new file mode 100644 index 00000000..b5c0c4e4 --- /dev/null +++ b/frontend/src/assets/svg/field_time.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/svg/field_value.svg b/frontend/src/assets/svg/field_value.svg new file mode 100644 index 00000000..b907e4f6 --- /dev/null +++ b/frontend/src/assets/svg/field_value.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index bbdbcaaf..f7e995c2 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -8,6 +8,8 @@ "Details": "Details" }, "variables": { + "please_select_date": "Please select a date", + "number_variable_error": "Please enter the correct numerical range.", "built_in": "Built-in", "normal_value": "Normal value", "​​cannot_be_empty": "Variable values ​​cannot be empty.", diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index 4cedf417..34c27650 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -24,6 +24,8 @@ "return_to_view": "화면으로 돌아가기" }, "variables": { + "please_select_date": "날짜를 선택하십시오", + "number_variable_error": "올바른 숫자 범위를 입력하십시오.", "built_in": "내장형", "normal_value": "정상값", "​​cannot_be_empty": "변수 값은 비어 있을 수 없습니다.", @@ -943,4 +945,4 @@ "to_doc": "API 보기", "trigger_limit": "최대 {0}개의 API 키 생성 지원" } -} +} \ No newline at end of file diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index f3b6e61d..66f81772 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -24,6 +24,8 @@ "return_to_view": "返回查看" }, "variables": { + "please_select_date": "请选择日期", + "number_variable_error": "请输入正确的数值范围", "built_in": "已内置", "normal_value": "常规值", "​​cannot_be_empty": "变量值不能为空", diff --git a/frontend/src/style.less b/frontend/src/style.less index 05e5b8b8..3798d8be 100644 --- a/frontend/src/style.less +++ b/frontend/src/style.less @@ -419,3 +419,13 @@ strong { margin-top: -7px; } } + +.text-variables.text-variables { + color: #3370ff; +} +.number-variables.number-variables { + color: #04b49c; +} +.datetime-variables.datetime-variables { + color: #3370ff; +} diff --git a/frontend/src/views/chat/ExecutionDetails.vue b/frontend/src/views/chat/ExecutionDetails.vue index a6545d5f..78dc4e63 100644 --- a/frontend/src/views/chat/ExecutionDetails.vue +++ b/frontend/src/views/chat/ExecutionDetails.vue @@ -173,6 +173,7 @@ defineExpose({ border: 1px solid #dee0e3; padding: 16px; margin-bottom: 8px; + cursor: pointer; .header { display: flex; diff --git a/frontend/src/views/chat/component/charts/Table.ts b/frontend/src/views/chat/component/charts/Table.ts index 2d7db13f..e769748c 100644 --- a/frontend/src/views/chat/component/charts/Table.ts +++ b/frontend/src/views/chat/component/charts/Table.ts @@ -7,6 +7,7 @@ import { type S2Options, type SortMethod, TableSheet, + type SortFuncParam, } from '@antv/s2' import { debounce, filter } from 'lodash-es' import { i18n } from '@/i18n' @@ -14,6 +15,60 @@ import '@antv/s2/dist/s2.min.css' const { t } = i18n.global +const createSmartSortFunc = (sortMethod: string) => { + const compareNumericString = (a: string, b: string): number => { + const isNegA = a.startsWith('-') + const isNegB = b.startsWith('-') + + // 负数 < 正数 + if (isNegA && !isNegB) return -1 + if (!isNegA && isNegB) return 1 + + const [intA, decA = ''] = isNegA ? a.slice(1).split('.') : a.split('.') + const [intB, decB = ''] = isNegB ? b.slice(1).split('.') : b.split('.') + + // 都是正数 + if (!isNegA && !isNegB) { + if (intA.length !== intB.length) return intA.length - intB.length + const intCmp = intA.localeCompare(intB) + if (intCmp !== 0) return intCmp + if (decA && decB) return decA.localeCompare(decB) + return decA ? 1 : decB ? -1 : 0 + } + + // 都是负数:绝对值大的实际值小,比较结果取反 + if (intA.length !== intB.length) return -(intA.length - intB.length) + const intCmp = intA.localeCompare(intB) + if (intCmp !== 0) return -intCmp + if (decA && decB) return -decA.localeCompare(decB) + return decA ? 1 : decB ? -1 : 0 + } + + return (params: SortFuncParam) => { + const { data, sortFieldId } = params + if (!data || data.length === 0) return data + const isAsc = sortMethod.toLowerCase() === 'asc' + return [...data].sort((a: any, b: any) => { + const valA = a[sortFieldId], + valB = b[sortFieldId] + if (valA == null) return isAsc ? -1 : 1 + if (valB == null) return isAsc ? 1 : -1 + const strA = String(valA), + strB = String(valB) + const isNumA = !isNaN(Number(strA)) && strA.trim() !== '' + const isNumB = !isNaN(Number(strB)) && strB.trim() !== '' + if (isNumA && !isNumB) return isAsc ? -1 : 1 + if (!isNumA && isNumB) return isAsc ? 1 : -1 + if (isNumA && isNumB) { + const cmp = compareNumericString(strA, strB) + return isAsc ? cmp : -cmp + } + const cmp = strA.localeCompare(strB) + return isAsc ? cmp : -cmp + }) + } +} + export class Table extends BaseChart { table?: TableSheet = undefined @@ -81,7 +136,17 @@ export class Table extends BaseChart { const sortOrder = ['none', 'desc', 'asc'] const nextMethod = sortOrder[(sortOrder.indexOf(currentMethod) + 1) % sortOrder.length] sortState[fieldId] = nextMethod - s2.groupSortByMethod(nextMethod === 'none' ? 'none' : (nextMethod as SortMethod), meta) + if (nextMethod === 'none') { + s2.emit(S2Event.RANGE_SORT, [{ sortFieldId: fieldId, sortMethod: 'none' as SortMethod }]) + } else { + s2.emit(S2Event.RANGE_SORT, [ + { + sortFieldId: fieldId, + sortMethod: nextMethod as SortMethod, + sortFunc: createSmartSortFunc(nextMethod), + }, + ]) + } s2.render() } } diff --git a/frontend/src/views/ds/DataTable.vue b/frontend/src/views/ds/DataTable.vue index 9af6d14a..1458ae4a 100644 --- a/frontend/src/views/ds/DataTable.vue +++ b/frontend/src/views/ds/DataTable.vue @@ -190,6 +190,15 @@ const editTable = () => { tableComment.value = currentTable.value.custom_comment tableDialog.value = true } +const changeChecked = () => { + datasourceApi.saveTable(currentTable.value).then(() => { + ElMessage({ + message: t('common.save_success'), + type: 'success', + showClose: true, + }) + }) +} const saveTable = () => { currentTable.value.custom_comment = tableComment.value datasourceApi.saveTable(currentTable.value).then(() => { @@ -473,7 +482,27 @@ const btnSelectClick = (val: any) => { class="info-table" >
-
{{ currentTable.table_name }}
+
+ {{ currentTable.table_name }} +
+ + + {{ currentTable.checked ? t('user.disable') : t('user.enable') }} +
+
{{ $t('about.remark') }}: {{ diff --git a/frontend/src/views/system/audit/index.vue b/frontend/src/views/system/audit/index.vue index 889bff52..d7af94a3 100644 --- a/frontend/src/views/system/audit/index.vue +++ b/frontend/src/views/system/audit/index.vue @@ -275,7 +275,7 @@ const initOptions = () => {
{
diff --git a/frontend/src/views/system/permission/auth-tree/RowAuth.vue b/frontend/src/views/system/permission/auth-tree/RowAuth.vue index 6b15e8a0..3548687c 100644 --- a/frontend/src/views/system/permission/auth-tree/RowAuth.vue +++ b/frontend/src/views/system/permission/auth-tree/RowAuth.vue @@ -2,6 +2,7 @@ import { ref, computed } from 'vue' import AuthTree from './AuthTree.vue' import { useI18n } from 'vue-i18n' +import { variablesApi } from '@/api/variables' const { t } = useI18n() const errorMessage = ref('') @@ -26,9 +27,10 @@ const svgDashinePath = computed(() => { return path }) -const init = (expressionTree: any) => { +const init = async (expressionTree: any) => { const { logic: lg = 'or', items = [] } = expressionTree logic.value = lg + await getVariables() relationList.value = dfsInit(items) } const submit = () => { @@ -65,6 +67,17 @@ const errorDetected = ({ filter_type, field_id, term, value, value_type, variabl } } } +const variables = ref([]) +const getVariables = () => { + return variablesApi.listAll().then((res: any) => { + variables.value = res || [] + }) +} + +const canPush = (value_type: any, variable_id: any) => { + if (value_type === 'normal') return true + return variables.value.some((ele) => ele.id === variable_id) +} const dfsInit = (arr: any[]) => { const elementList: any[] = [] arr.forEach((ele: any) => { @@ -85,16 +98,18 @@ const dfsInit = (arr: any[]) => { variable_id = '', } = ele const { name } = field || {} - elementList.push({ - enum_value: enum_value.join(','), - field_id, - filter_type, - term, - value, - name, - value_type, - variable_id, - }) + if (canPush(value_type, variable_id)) { + elementList.push({ + enum_value: enum_value.join(','), + field_id, + filter_type, + term, + value, + name, + value_type, + variable_id, + }) + } } }) return elementList diff --git a/frontend/src/views/system/professional/index.vue b/frontend/src/views/system/professional/index.vue index 62491111..e104187e 100644 --- a/frontend/src/views/system/professional/index.vue +++ b/frontend/src/views/system/professional/index.vue @@ -470,7 +470,7 @@ const changeStatus = (id: any, val: any) => {
{
{
+
+ + + + {{ item.name }} +
- ([ { type: 'enum', @@ -995,10 +1018,9 @@ const formatVariableValues = () => { if (!state.form.system_variables?.length) return [] return state.form.system_variables.map((ele: any) => ({ variableId: ele.variableId, - variableValues: - variableValueMap.value[ele.variableId].var_type === 'number' - ? [ele.variableValue] - : ele.variableValues, + variableValues: ['number', 'datetime'].includes(variableValueMap.value[ele.variableId].var_type) + ? [ele.variableValue] + : ele.variableValues, })) } @@ -1055,12 +1077,12 @@ const validateSystemVariables = () => { if (system_variables?.length) { return system_variables.some((ele: any) => { const obj = variableValueMap.value[ele.variableId] - if (obj.var_type !== 'number' && !ele.variableValues.length) { + if (obj.var_type === 'text' && !ele.variableValues.length) { ElMessage.error(t('variables.​​cannot_be_empty')) return true } - if (obj.var_type === 'number' && !ele.variableValue) { + if (obj.var_type === 'number' && [null, undefined, ''].includes(ele.variableValue)) { ElMessage.error(t('variables.​​cannot_be_empty')) return true } @@ -1075,12 +1097,9 @@ const validateSystemVariables = () => { if (obj.var_type === 'datetime') { const [min, max] = obj.value - const [minVal, maxVal] = ele.variableValues if ( - +new Date(minVal) > +new Date(max) || - +new Date(maxVal) < +new Date(min) || - +new Date(maxVal) > +new Date(max) || - +new Date(minVal) < +new Date(min) + +new Date(ele.variableValue) > +new Date(max) || + +new Date(ele.variableValue) < +new Date(min) ) { ElMessage.error( t('variables.1_to_100_de', { diff --git a/frontend/src/views/system/variables/index.vue b/frontend/src/views/system/variables/index.vue index 6f92b191..3ce3ac23 100644 --- a/frontend/src/views/system/variables/index.vue +++ b/frontend/src/views/system/variables/index.vue @@ -7,6 +7,9 @@ import IconOpeDelete from '@/assets/svg/icon_delete.svg' import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg' import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue' import { useI18n } from 'vue-i18n' +import field_text from '@/assets/svg/field_text.svg' +import field_time from '@/assets/svg/field_time.svg' +import field_value from '@/assets/svg/field_value.svg' import { cloneDeep } from 'lodash-es' interface Form { @@ -21,6 +24,11 @@ const multipleSelectionAll = ref([]) const keywords = ref('') const oldKeywords = ref('') const searchLoading = ref(false) +const iconMap = { + text: field_text, + number: field_value, + datetime: field_time, +} const selectable = (row: any) => { return ![1, 2, 3].includes(row.id) @@ -74,7 +82,7 @@ const deleteBatchUser = () => { message: t('dashboard.delete_success'), }) multipleSelectionAll.value = [] - search() + handleCurrentChange(1) }) }) } @@ -94,7 +102,7 @@ const deleteHandler = (row: any) => { type: 'success', message: t('dashboard.delete_success'), }) - search() + handleCurrentChange(1) }) }) } @@ -181,7 +189,12 @@ const validateValue = (_: any, value: any, callback: any) => { } return } - if (value.some((ele: any) => ele === '')) { + if (var_type === 'datetime') { + if (value === null) { + callback(new Error(t('datasource.please_enter') + t('common.empty') + t('variables.date'))) + } + } + if (value.some((ele: any) => ele === '' || ele === null)) { callback( new Error( t('datasource.please_enter') + @@ -229,12 +242,23 @@ const saveHandler = () => { obj.value = [...new Set(obj.value)] } + if (obj.var_type === 'number') { + const [min = 0, max = 0] = obj.value + if (min > max) { + ElMessage({ + type: 'error', + message: t('variables.number_variable_error'), + }) + return + } + } + variablesApi.save(obj).then(() => { ElMessage({ type: 'success', message: t('common.save_success'), }) - search() + handleCurrentChange(1) onFormClose() }) } @@ -302,7 +326,7 @@ const handleCurrentChange = (val: number) => {