Compare commits
5 Commits
5755b02b38
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddba2dc3e1 | ||
|
|
730d453371 | ||
|
|
9edb9d6aca | ||
|
|
34eb6bea56 | ||
|
|
e5ca68423c |
BIN
protoc-frontend.exe
LFS
BIN
protoc-frontend.exe
LFS
Binary file not shown.
@@ -1,3 +1,17 @@
|
|||||||
module protoc-frontend-server
|
module protoc-frontend-server
|
||||||
|
|
||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/richardlehane/mscfb v1.0.6 // indirect
|
||||||
|
github.com/richardlehane/msoleps v1.0.6 // indirect
|
||||||
|
github.com/tiendc/go-deepcopy v1.7.2 // indirect
|
||||||
|
github.com/xuri/efp v0.0.1 // indirect
|
||||||
|
github.com/xuri/excelize/v2 v2.10.1 // indirect
|
||||||
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||||
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
|
golang.org/x/net v0.50.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
22
server/go.sum
Normal file
22
server/go.sum
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
|
||||||
|
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
|
||||||
|
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
|
||||||
|
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
|
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
|
||||||
|
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||||
|
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||||
|
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||||
|
github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0=
|
||||||
|
github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc=
|
||||||
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||||
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed dist/*
|
//go:embed dist/*
|
||||||
@@ -31,7 +33,6 @@ func main() {
|
|||||||
originalDirector := proxy.Director
|
originalDirector := proxy.Director
|
||||||
proxy.Director = func(r *http.Request) {
|
proxy.Director = func(r *http.Request) {
|
||||||
originalDirector(r)
|
originalDirector(r)
|
||||||
// Rewrite /api/... -> /...
|
|
||||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
|
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
|
||||||
if r.URL.Path == "" {
|
if r.URL.Path == "" {
|
||||||
r.URL.Path = "/"
|
r.URL.Path = "/"
|
||||||
@@ -46,31 +47,34 @@ func main() {
|
|||||||
}
|
}
|
||||||
fileServer := http.FileServer(http.FS(distSub))
|
fileServer := http.FileServer(http.FS(distSub))
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
// API proxy
|
// API routes
|
||||||
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
|
r.Route("/api", func(api chi.Router) {
|
||||||
proxy.ServeHTTP(w, r)
|
// Protocol generation handled locally (xlsx fill via excelize)
|
||||||
|
api.Post("/protocols/", protocolHandler(*apiTarget))
|
||||||
|
|
||||||
|
// All other /api/* requests proxied to Python backend
|
||||||
|
api.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
proxy.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// SPA: serve static files, fallback to index.html
|
// SPA: serve static files, fallback to index.html
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
|
||||||
// Try to serve the file directly
|
|
||||||
path := strings.TrimPrefix(r.URL.Path, "/")
|
path := strings.TrimPrefix(r.URL.Path, "/")
|
||||||
if path == "" {
|
if path == "" {
|
||||||
path = "index.html"
|
path = "index.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if file exists in embedded FS
|
|
||||||
if f, err := distSub.Open(path); err == nil {
|
if f, err := distSub.Open(path); err == nil {
|
||||||
f.Close()
|
f.Close()
|
||||||
fileServer.ServeHTTP(w, r)
|
fileServer.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to index.html for SPA routing
|
|
||||||
indexFile, err := fs.ReadFile(distSub, "index.html")
|
indexFile, err := fs.ReadFile(distSub, "index.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "index.html not found", http.StatusInternalServerError)
|
http.Error(w, "index.html not found", http.StatusInternalServerError)
|
||||||
@@ -84,7 +88,7 @@ func main() {
|
|||||||
log.Printf("Serving on http://localhost%s", addr)
|
log.Printf("Serving on http://localhost%s", addr)
|
||||||
log.Printf("API proxy -> %s", *apiTarget)
|
log.Printf("API proxy -> %s", *apiTarget)
|
||||||
|
|
||||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
if err := http.ListenAndServe(addr, r); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
107
server/protocol.go
Normal file
107
server/protocol.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateProtocolRequest struct {
|
||||||
|
TemplateID string `json:"template_id"`
|
||||||
|
FileID string `json:"file_id"`
|
||||||
|
CellsToUpdate map[string]any `json:"cells_to_update"`
|
||||||
|
PrintArea string `json:"print_area"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolHandler(backendURL string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse request body
|
||||||
|
var req CreateProtocolRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch file binary from Python backend
|
||||||
|
fileURL := fmt.Sprintf("%s/files/%s/content", backendURL, req.FileID)
|
||||||
|
resp, err := http.Get(fileURL)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to fetch file: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
http.Error(w, fmt.Sprintf("backend returned %d when fetching file", resp.StatusCode), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to read file content: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open xlsx with excelize
|
||||||
|
f, err := excelize.OpenReader(bytes.NewReader(fileBytes))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to open xlsx: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Get the first sheet
|
||||||
|
sheetName := f.GetSheetName(0)
|
||||||
|
if sheetName == "" {
|
||||||
|
http.Error(w, "no sheets found in xlsx", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill cells
|
||||||
|
for cellAddr, value := range req.CellsToUpdate {
|
||||||
|
if err := setCellValue(f, sheetName, cellAddr, value); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to set cell %s: %v", cellAddr, err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write result to buffer
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := f.Write(&buf); err != nil {
|
||||||
|
http.Error(w, "failed to write xlsx: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
protocolID := uuid.New().String()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="protocol_%s.xlsx"`, protocolID))
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
|
||||||
|
w.Write(buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCellValue(f *excelize.File, sheet, cell string, value any) error {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case float64:
|
||||||
|
// JSON numbers are float64; check if it's actually an integer
|
||||||
|
if v == float64(int64(v)) {
|
||||||
|
return f.SetCellInt(sheet, cell, int64(v))
|
||||||
|
}
|
||||||
|
return f.SetCellFloat(sheet, cell, v, -1, 64)
|
||||||
|
case string:
|
||||||
|
return f.SetCellStr(sheet, cell, v)
|
||||||
|
case bool:
|
||||||
|
return f.SetCellBool(sheet, cell, v)
|
||||||
|
case nil:
|
||||||
|
return f.SetCellStr(sheet, cell, "")
|
||||||
|
default:
|
||||||
|
return f.SetCellStr(sheet, cell, fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '@/lib/hooks/useToast'
|
} from '@/lib/hooks/useToast'
|
||||||
import {
|
import {
|
||||||
ElementsCreation,
|
ElementsCreation,
|
||||||
|
FunctionsWikiPage,
|
||||||
ProtocolCreation,
|
ProtocolCreation,
|
||||||
StandardsManagementPage,
|
StandardsManagementPage,
|
||||||
TemplateEditPage,
|
TemplateEditPage,
|
||||||
@@ -59,6 +60,10 @@ const App: FC = () => {
|
|||||||
path="standards-management"
|
path="standards-management"
|
||||||
element={<StandardsManagementPage />}
|
element={<StandardsManagementPage />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="functions-wiki"
|
||||||
|
element={<FunctionsWikiPage />}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</TemplateProvider>
|
</TemplateProvider>
|
||||||
|
|||||||
@@ -184,6 +184,156 @@ describe('Workbook — формулы', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ==================== ЕСЛИ с операторами сравнения ====================
|
||||||
|
|
||||||
|
describe('ЕСЛИ с операторами = >= <>', () => {
|
||||||
|
it('ЕСЛИ(A1=5, ...) — равенство числу (совпадает)', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 5)
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1=5, "Пять", "Другое")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('Пять')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ(A1=5, ...) — равенство числу (не совпадает)', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 7)
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1=5, "Пять", "Другое")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('Другое')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ(A1=B1, ...) — сравнение двух ячеек (равны)', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 10)
|
||||||
|
sheet.setCell('B1', 10)
|
||||||
|
sheet.setCell('C1', '=ЕСЛИ(A1=B1, "Равны", "Не равны")')
|
||||||
|
expect(wb.getValue('Sheet1!C1')).toBe('Равны')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ(A1=B1, ...) — сравнение двух ячеек (не равны)', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 10)
|
||||||
|
sheet.setCell('B1', 20)
|
||||||
|
sheet.setCell('C1', '=ЕСЛИ(A1=B1, "Равны", "Не равны")')
|
||||||
|
expect(wb.getValue('Sheet1!C1')).toBe('Не равны')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ(A1>=100, ...) — больше или равно (истина)', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 100)
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1>=100, "Сотня+", A1)')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('Сотня+')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ(A1>=100, ...) — больше или равно (ложь)', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 42)
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1>=100, "Сотня+", A1)')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe(42)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ(A1>10, ...) — строгое больше', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 15)
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1>10, "Да", "Нет")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('Да')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ с И и операторами >= <=', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 5)
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(И(A1>=0, A1<=10), "В диапазоне", "Вне")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('В диапазоне')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ с И и операторами >= <= — вне диапазона', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 15)
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(И(A1>=0, A1<=10), "В диапазоне", "Вне")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('Вне')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ(A1>10, ...) — строгое больше (ложь)', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 5)
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1>10, "Да", "Нет")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('Нет')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ с БОЛЬШЕ — истина', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(БОЛЬШЕ(5, 3), 1, 0)')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ с БОЛЬШЕ — ложь', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(БОЛЬШЕ(1, 3), 1, 0)')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ с равенством текста в ячейке', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 'Готово')
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1="Готово", "✓", "✗")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('✓')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ с неравенством текста в ячейке', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 'В работе')
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1="Готово", "✓", "✗")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('✗')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Оператор <> (не равно)', () => {
|
||||||
|
it('ЕСЛИ(A1<>0, ...) — не равно (истина)', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 5)
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1<>0, "Не ноль", "Ноль")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('Не ноль')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ(A1<>0, ...) — не равно (ложь)', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 0)
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1<>0, "Не ноль", "Ноль")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('Ноль')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ(A1<>B1, ...) — две ячейки не равны', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 10)
|
||||||
|
sheet.setCell('B1', 20)
|
||||||
|
sheet.setCell('C1', '=ЕСЛИ(A1<>B1, "Разные", "Одинаковые")')
|
||||||
|
expect(wb.getValue('Sheet1!C1')).toBe('Разные')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ЕСЛИ(A1<>B1, ...) — две ячейки равны', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 10)
|
||||||
|
sheet.setCell('B1', 10)
|
||||||
|
sheet.setCell('C1', '=ЕСЛИ(A1<>B1, "Разные", "Одинаковые")')
|
||||||
|
expect(wb.getValue('Sheet1!C1')).toBe('Одинаковые')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('<> внутри строкового литерала не трогается', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 1)
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1=1, "a<>b", "нет")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('a<>b')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Операторы внутри строковых литералов не трогаются', () => {
|
||||||
|
it('= внутри строкового литерала остаётся как есть', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 1)
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1=1, "x=y", "нет")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('x=y')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// ==================== ЕПУСТО ====================
|
// ==================== ЕПУСТО ====================
|
||||||
|
|
||||||
describe('ЕПУСТО в формуле', () => {
|
describe('ЕПУСТО в формуле', () => {
|
||||||
@@ -679,3 +829,130 @@ describe('CellUtils', () => {
|
|||||||
expect(CellUtils.createCellName('B', 3)).toBe('B3')
|
expect(CellUtils.createCellName('B', 3)).toBe('B3')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ==================== Edge Cases ====================
|
||||||
|
|
||||||
|
describe('Edge Cases функций через движок', () => {
|
||||||
|
// 1. ОКРУГЛ с отрицательными разрядами: ОКРУГЛ(1234, -2) = 1200 в Excel
|
||||||
|
describe('ОКРУГЛ с отрицательными разрядами', () => {
|
||||||
|
it('ОКРУГЛ(1234, -2) должно дать 1200', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', '=ОКРУГЛ(1234, -2)')
|
||||||
|
expect(wb.getValue('Sheet1!A1')).toBe(1200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ОКРУГЛ(1550, -3) должно дать 2000', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', '=ОКРУГЛ(1550, -3)')
|
||||||
|
expect(wb.getValue('Sheet1!A1')).toBe(2000)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. ЕСЛИ с числом как условие (0 = ложь, не-0 = истина)
|
||||||
|
describe('ЕСЛИ с числовым условием', () => {
|
||||||
|
it('0 как ложь', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 0)
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1, "да", "нет")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('нет')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ненулевое число как истина', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 42)
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1, "да", "нет")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('да')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. СЦЕПИТЬ с числами
|
||||||
|
describe('СЦЕПИТЬ с числами', () => {
|
||||||
|
it('число конвертируется в строку', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 42)
|
||||||
|
sheet.setCell('B1', '=СЦЕПИТЬ("итого: ", A1)')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('итого: 42')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. Несколько = в одной формуле
|
||||||
|
describe('Несколько = в одной формуле', () => {
|
||||||
|
it('И(A1=1, B1=2) — оба true', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 1)
|
||||||
|
sheet.setCell('B1', 2)
|
||||||
|
sheet.setCell('C1', '=ЕСЛИ(И(A1=1, B1=2), "да", "нет")')
|
||||||
|
expect(wb.getValue('Sheet1!C1')).toBe('да')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('И(A1=1, B1=2) — одно false', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 1)
|
||||||
|
sheet.setCell('B1', 99)
|
||||||
|
sheet.setCell('C1', '=ЕСЛИ(И(A1=1, B1=2), "да", "нет")')
|
||||||
|
expect(wb.getValue('Sheet1!C1')).toBe('нет')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 5. Пустая ячейка в сравнении (движок подставляет 0)
|
||||||
|
describe('Пустая ячейка в сравнении с =', () => {
|
||||||
|
it('пустая ячейка = 0', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
// A1 не установлена — пустая, движок подставит 0
|
||||||
|
sheet.setCell('B1', '=ЕСЛИ(A1=0, "пусто/ноль", "есть значение")')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe('пусто/ноль')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 6. СТЕПЕНЬ(0, 0) — в Excel = 1
|
||||||
|
describe('СТЕПЕНЬ edge cases', () => {
|
||||||
|
it('СТЕПЕНЬ(0, 0) = 1', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', '=СТЕПЕНЬ(0, 0)')
|
||||||
|
expect(wb.getValue('Sheet1!A1')).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 7. МАКС — вариадический (как в Excel)
|
||||||
|
describe('МАКС', () => {
|
||||||
|
// Обратная совместимость: 2 аргумента (как было раньше)
|
||||||
|
it('МАКС(3, 7) = 7 — обратная совместимость', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', '=МАКС(3, 7)')
|
||||||
|
expect(wb.getValue('Sheet1!A1')).toBe(7)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('МАКС(10, 5) = 10 — обратная совместимость', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', '=МАКС(10, 5)')
|
||||||
|
expect(wb.getValue('Sheet1!A1')).toBe(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('МАКС(-5, 3) = 3 — обратная совместимость с отрицательными', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', '=МАКС(-5, 3)')
|
||||||
|
expect(wb.getValue('Sheet1!A1')).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('МАКС со ссылками на ячейки — обратная совместимость', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', 25)
|
||||||
|
sheet.setCell('A2', 10)
|
||||||
|
sheet.setCell('B1', '=МАКС(A1, A2)')
|
||||||
|
expect(wb.getValue('Sheet1!B1')).toBe(25)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Новое: 3+ аргумента
|
||||||
|
it('МАКС(3, 7, 100) = 100 — вариадический', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', '=МАКС(3, 7, 100)')
|
||||||
|
expect(wb.getValue('Sheet1!A1')).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('МАКС(1, 2, 3, 4, 5) = 5', () => {
|
||||||
|
const { wb, sheet } = createWorkbookWithSheet()
|
||||||
|
sheet.setCell('A1', '=МАКС(1, 2, 3, 4, 5)')
|
||||||
|
expect(wb.getValue('Sheet1!A1')).toBe(5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -131,7 +131,9 @@ export class Cell {
|
|||||||
try {
|
try {
|
||||||
const cell = this.sheet.workbook.getCell(qualifiedRef)
|
const cell = this.sheet.workbook.getCell(qualifiedRef)
|
||||||
const raw = cell.raw
|
const raw = cell.raw
|
||||||
return (raw === null || raw === undefined || raw === '') ? 'true' : 'false'
|
return raw === null || raw === undefined || raw === ''
|
||||||
|
? 'true'
|
||||||
|
: 'false'
|
||||||
} catch {
|
} catch {
|
||||||
return 'true'
|
return 'true'
|
||||||
}
|
}
|
||||||
@@ -230,6 +232,21 @@ export class Cell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Конвертируем Excel-операторы сравнения в JS-операторы
|
||||||
|
// Сохраняем строковые литералы, чтобы не трогать операторы внутри них
|
||||||
|
const strings: string[] = []
|
||||||
|
code = code.replace(/"[^"]*"/g, m => {
|
||||||
|
strings.push(m)
|
||||||
|
return `__STR${strings.length - 1}__`
|
||||||
|
})
|
||||||
|
// <> → !== (не равно, как в Excel)
|
||||||
|
code = code.replace(/<>/g, '!==')
|
||||||
|
// >= и <= уже валидны в JS, пропускаем
|
||||||
|
// Одиночное = (не ==, !=, >=, <=) → === (равенство вместо присваивания)
|
||||||
|
code = code.replace(/(?<![<>!=])=(?!=)/g, '===')
|
||||||
|
// Восстанавливаем строковые литералы
|
||||||
|
code = code.replace(/__STR(\d+)__/g, (_, i) => strings[Number(i)])
|
||||||
|
|
||||||
// Создаём контекст для выполнения
|
// Создаём контекст для выполнения
|
||||||
const context = {
|
const context = {
|
||||||
__functions__: functions,
|
__functions__: functions,
|
||||||
|
|||||||
@@ -41,14 +41,23 @@ describe('DEFAULT_FUNCTIONS', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('МАКС', () => {
|
describe('МАКС', () => {
|
||||||
it('возвращает максимум', () => {
|
it('возвращает максимум из двух', () => {
|
||||||
expect(DEFAULT_FUNCTIONS.МАКС(3, 7)).toBe(7)
|
expect(DEFAULT_FUNCTIONS.МАКС(3, 7)).toBe(7)
|
||||||
})
|
})
|
||||||
it('ошибка при не-числах', () => {
|
it('возвращает максимум из нескольких', () => {
|
||||||
expect(DEFAULT_FUNCTIONS.МАКС('a' as any, 1)).toBe('#ЗНАЧ!')
|
expect(DEFAULT_FUNCTIONS.МАКС(3, 7, 100)).toBe(100)
|
||||||
})
|
})
|
||||||
it('ошибка при Infinity', () => {
|
it('работает с массивом', () => {
|
||||||
expect(DEFAULT_FUNCTIONS.МАКС(Infinity, 1)).toBe('#ЧИСЛО!')
|
expect(DEFAULT_FUNCTIONS.МАКС([3, 7, 1])).toBe(7)
|
||||||
|
})
|
||||||
|
it('игнорирует не-числа', () => {
|
||||||
|
expect(DEFAULT_FUNCTIONS.МАКС('a' as any, 1)).toBe(1)
|
||||||
|
})
|
||||||
|
it('игнорирует Infinity', () => {
|
||||||
|
expect(DEFAULT_FUNCTIONS.МАКС(Infinity, 1)).toBe(1)
|
||||||
|
})
|
||||||
|
it('ошибка без валидных чисел', () => {
|
||||||
|
expect(DEFAULT_FUNCTIONS.МАКС('a' as any, 'b' as any)).toBe('#ЗНАЧ!')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -67,18 +67,11 @@ export const DEFAULT_FUNCTIONS: Record<string, ExcelFunction> = {
|
|||||||
const flatArgs = args.flat()
|
const flatArgs = args.flat()
|
||||||
return flatArgs.filter(val => typeof val === 'number').length
|
return flatArgs.filter(val => typeof val === 'number').length
|
||||||
},
|
},
|
||||||
МАКС: (число1: number, число2: number) => {
|
МАКС: (...args: any[]) => {
|
||||||
// Проверка валидности входных параметров
|
const flatArgs = args.flat()
|
||||||
if (typeof число1 !== 'number' || typeof число2 !== 'number') {
|
const validArgs = flatArgs.filter(val => typeof val === 'number' && isFinite(val))
|
||||||
return '#ЗНАЧ!' // Ошибка значения
|
if (validArgs.length === 0) return '#ЗНАЧ!'
|
||||||
}
|
return Math.max(...validArgs)
|
||||||
|
|
||||||
if (!isFinite(число1) || !isFinite(число2)) {
|
|
||||||
return '#ЧИСЛО!' // Ошибка числа
|
|
||||||
}
|
|
||||||
|
|
||||||
// Возвращаем максимальное из двух чисел
|
|
||||||
return Math.max(число1, число2)
|
|
||||||
},
|
},
|
||||||
МИН: (...args: any[]) => {
|
МИН: (...args: any[]) => {
|
||||||
const flatArgs = args.flat()
|
const flatArgs = args.flat()
|
||||||
|
|||||||
669
src/page/functions-wiki/Page.tsx
Normal file
669
src/page/functions-wiki/Page.tsx
Normal file
@@ -0,0 +1,669 @@
|
|||||||
|
import { ArrowLeft, BookOpen, Calculator, Hash, Search, Sigma, Type } from 'lucide-react'
|
||||||
|
import { FC, useMemo, useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
interface FunctionInfo {
|
||||||
|
name: string
|
||||||
|
category: string
|
||||||
|
description: string
|
||||||
|
syntax: string
|
||||||
|
args: { name: string; description: string; optional?: boolean }[]
|
||||||
|
examples: { formula: string; result: string }[]
|
||||||
|
notes?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FUNCTION_CATALOG: FunctionInfo[] = [
|
||||||
|
// Случайные числа
|
||||||
|
{
|
||||||
|
name: 'СЛУЧ',
|
||||||
|
category: 'Случайные числа',
|
||||||
|
description: 'Возвращает случайное число от 0 до 1.',
|
||||||
|
syntax: 'СЛУЧ()',
|
||||||
|
args: [],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=СЛУЧ()', result: '0.7231...' },
|
||||||
|
],
|
||||||
|
notes: 'Волатильная функция — пересчитывается при каждом изменении.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'СЛУЧМЕЖДУ',
|
||||||
|
category: 'Случайные числа',
|
||||||
|
description: 'Возвращает случайное целое число в заданном диапазоне.',
|
||||||
|
syntax: 'СЛУЧМЕЖДУ(низ,верх)',
|
||||||
|
args: [
|
||||||
|
{ name: 'низ', description: 'Нижняя граница диапазона' },
|
||||||
|
{ name: 'верх', description: 'Верхняя граница диапазона' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=СЛУЧМЕЖДУ(1,10)', result: '7' },
|
||||||
|
{ formula: '=СЛУЧМЕЖДУ(100,200)', result: '142' },
|
||||||
|
],
|
||||||
|
notes: 'Волатильная функция.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'СЛУЧДР',
|
||||||
|
category: 'Случайные числа',
|
||||||
|
description: 'Возвращает случайное дробное число в заданном диапазоне с указанным количеством знаков после запятой.',
|
||||||
|
syntax: 'СЛУЧДР(низ,верх,знаков)',
|
||||||
|
args: [
|
||||||
|
{ name: 'низ', description: 'Нижняя граница диапазона' },
|
||||||
|
{ name: 'верх', description: 'Верхняя граница диапазона' },
|
||||||
|
{ name: 'знаков', description: 'Количество знаков после запятой (0–15)', optional: true },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=СЛУЧДР(1,10,2)', result: '4.73' },
|
||||||
|
{ formula: '=СЛУЧДР(0,1,4)', result: '0.5182' },
|
||||||
|
],
|
||||||
|
notes: 'Волатильная функция. Если низ > верх, значения меняются местами автоматически.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Математические
|
||||||
|
{
|
||||||
|
name: 'СУММ',
|
||||||
|
category: 'Математические',
|
||||||
|
description: 'Суммирует числовые значения. Нечисловые значения игнорируются.',
|
||||||
|
syntax: 'СУММ(число1,число2,...)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число1,число2,...', description: 'Числа или диапазоны ячеек для суммирования' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=СУММ(1,2,3)', result: '6' },
|
||||||
|
{ formula: '=СУММ(A1:A5)', result: 'Сумма ячеек A1–A5' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'СРЗНАЧ',
|
||||||
|
category: 'Математические',
|
||||||
|
description: 'Вычисляет среднее арифметическое числовых значений.',
|
||||||
|
syntax: 'СРЗНАЧ(число1,число2,...)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число1,число2,...', description: 'Числа или диапазоны ячеек' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=СРЗНАЧ(10,20,30)', result: '20' },
|
||||||
|
{ formula: '=СРЗНАЧ(A1:A3)', result: 'Среднее значений A1–A3' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'СЧЁТ',
|
||||||
|
category: 'Математические',
|
||||||
|
description: 'Подсчитывает количество числовых значений в аргументах.',
|
||||||
|
syntax: 'СЧЁТ(значение1,значение2,...)',
|
||||||
|
args: [
|
||||||
|
{ name: 'значение1,значение2,...', description: 'Значения или диапазоны ячеек' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=СЧЁТ(1,"текст",3,5)', result: '3' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'МАКС',
|
||||||
|
category: 'Математические',
|
||||||
|
description: 'Возвращает наибольшее из двух чисел.',
|
||||||
|
syntax: 'МАКС(число1,число2)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число1', description: 'Первое число' },
|
||||||
|
{ name: 'число2', description: 'Второе число' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=МАКС(10,25)', result: '25' },
|
||||||
|
{ formula: '=МАКС(-5,3)', result: '3' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'МИН',
|
||||||
|
category: 'Математические',
|
||||||
|
description: 'Возвращает наименьшее числовое значение из аргументов.',
|
||||||
|
syntax: 'МИН(число1,число2,...)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число1,число2,...', description: 'Числа или диапазоны ячеек' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=МИН(10,5,20)', result: '5' },
|
||||||
|
{ formula: '=МИН(A1:A3)', result: 'Минимальное из A1–A3' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ABS',
|
||||||
|
category: 'Математические',
|
||||||
|
description: 'Возвращает абсолютное значение (модуль) числа.',
|
||||||
|
syntax: 'ABS(число)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число', description: 'Число, модуль которого нужно найти' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=ABS(-15)', result: '15' },
|
||||||
|
{ formula: '=ABS(7)', result: '7' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'КОРЕНЬ',
|
||||||
|
category: 'Математические',
|
||||||
|
description: 'Возвращает квадратный корень числа.',
|
||||||
|
syntax: 'КОРЕНЬ(число)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число', description: 'Неотрицательное число' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=КОРЕНЬ(25)', result: '5' },
|
||||||
|
{ formula: '=КОРЕНЬ(2)', result: '1.4142...' },
|
||||||
|
],
|
||||||
|
notes: 'Возвращает #ЧИСЛО! для отрицательных чисел.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'СТЕПЕНЬ',
|
||||||
|
category: 'Математические',
|
||||||
|
description: 'Возводит число в указанную степень.',
|
||||||
|
syntax: 'СТЕПЕНЬ(число,степень)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число', description: 'Основание' },
|
||||||
|
{ name: 'степень', description: 'Показатель степени' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=СТЕПЕНЬ(2,10)', result: '1024' },
|
||||||
|
{ formula: '=СТЕПЕНЬ(5,2)', result: '25' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Округление
|
||||||
|
{
|
||||||
|
name: 'ОКРУГЛ',
|
||||||
|
category: 'Округление',
|
||||||
|
description: 'Округляет число до указанного количества разрядов.',
|
||||||
|
syntax: 'ОКРУГЛ(число,разряды)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число', description: 'Число для округления' },
|
||||||
|
{ name: 'разряды', description: 'Количество десятичных разрядов', optional: true },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=ОКРУГЛ(3.456,2)', result: '3.46' },
|
||||||
|
{ formula: '=ОКРУГЛ(3.456,0)', result: '3' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ОКРУГЛВВЕРХ',
|
||||||
|
category: 'Округление',
|
||||||
|
description: 'Округляет число вверх (от нуля) до указанного количества разрядов.',
|
||||||
|
syntax: 'ОКРУГЛВВЕРХ(число,разряды)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число', description: 'Число для округления' },
|
||||||
|
{ name: 'разряды', description: 'Количество десятичных разрядов', optional: true },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=ОКРУГЛВВЕРХ(3.21,1)', result: '3.3' },
|
||||||
|
{ formula: '=ОКРУГЛВВЕРХ(3.1,0)', result: '4' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ОКРУГЛВНИЗ',
|
||||||
|
category: 'Округление',
|
||||||
|
description: 'Округляет число вниз (к нулю) до указанного количества разрядов.',
|
||||||
|
syntax: 'ОКРУГЛВНИЗ(число,разряды)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число', description: 'Число для округления' },
|
||||||
|
{ name: 'разряды', description: 'Количество десятичных разрядов', optional: true },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=ОКРУГЛВНИЗ(3.89,1)', result: '3.8' },
|
||||||
|
{ formula: '=ОКРУГЛВНИЗ(3.9,0)', result: '3' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ОКРВВЕРХ',
|
||||||
|
category: 'Округление',
|
||||||
|
description: 'Округляет число вверх до ближайшего кратного.',
|
||||||
|
syntax: 'ОКРВВЕРХ(число,кратное)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число', description: 'Число для округления' },
|
||||||
|
{ name: 'кратное', description: 'Кратное, до которого округляется', optional: true },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=ОКРВВЕРХ(7,5)', result: '10' },
|
||||||
|
{ formula: '=ОКРВВЕРХ(23,10)', result: '30' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ОКРВНИЗ',
|
||||||
|
category: 'Округление',
|
||||||
|
description: 'Округляет число вниз до ближайшего кратного.',
|
||||||
|
syntax: 'ОКРВНИЗ(число,кратное)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число', description: 'Число для округления' },
|
||||||
|
{ name: 'кратное', description: 'Кратное, до которого округляется', optional: true },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=ОКРВНИЗ(7,5)', result: '5' },
|
||||||
|
{ formula: '=ОКРВНИЗ(27,10)', result: '20' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ЦЕЛОЕ',
|
||||||
|
category: 'Округление',
|
||||||
|
description: 'Округляет число вниз до ближайшего целого.',
|
||||||
|
syntax: 'ЦЕЛОЕ(число)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число', description: 'Число для округления' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=ЦЕЛОЕ(7.8)', result: '7' },
|
||||||
|
{ formula: '=ЦЕЛОЕ(-3.2)', result: '-4' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ОТБР',
|
||||||
|
category: 'Округление',
|
||||||
|
description: 'Отбрасывает дробную часть числа до указанного количества разрядов.',
|
||||||
|
syntax: 'ОТБР(число,разряды)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число', description: 'Число для усечения' },
|
||||||
|
{ name: 'разряды', description: 'Количество десятичных разрядов', optional: true },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=ОТБР(7.89,1)', result: '7.8' },
|
||||||
|
{ formula: '=ОТБР(-3.7,0)', result: '-3' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ОКРУГЛТ',
|
||||||
|
category: 'Округление',
|
||||||
|
description: 'Округляет число до ближайшего кратного.',
|
||||||
|
syntax: 'ОКРУГЛТ(число,кратное)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число', description: 'Число для округления' },
|
||||||
|
{ name: 'кратное', description: 'Кратное, до которого округляется' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=ОКРУГЛТ(7,3)', result: '6' },
|
||||||
|
{ formula: '=ОКРУГЛТ(10,3)', result: '9' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Логические
|
||||||
|
{
|
||||||
|
name: 'ЕСЛИ',
|
||||||
|
category: 'Логические',
|
||||||
|
description: 'Возвращает одно значение, если условие истинно, и другое — если ложно.',
|
||||||
|
syntax: 'ЕСЛИ(условие,истина,ложь)',
|
||||||
|
args: [
|
||||||
|
{ name: 'условие', description: 'Логическое выражение для проверки' },
|
||||||
|
{ name: 'истина', description: 'Значение, если условие истинно' },
|
||||||
|
{ name: 'ложь', description: 'Значение, если условие ложно' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=ЕСЛИ(A1>10, "Да", "Нет")', result: '"Да" если A1 > 10' },
|
||||||
|
{ formula: '=ЕСЛИ(A1=5, "Пять", "Другое")', result: '"Пять" если A1 равно 5' },
|
||||||
|
{ formula: '=ЕСЛИ(A1=B1, "Равны", "Не равны")', result: 'Сравнение двух ячеек на равенство' },
|
||||||
|
{ formula: '=ЕСЛИ(A1>=100, "Сотня+", A1)', result: '"Сотня+" если A1 >= 100, иначе значение A1' },
|
||||||
|
{ formula: '=ЕСЛИ(БОЛЬШЕ(5, 3), 1, 0)', result: '1' },
|
||||||
|
{ formula: '=ЕСЛИ(A1="Готово", "✓", "✗")', result: '"✓" если A1 содержит текст "Готово"' },
|
||||||
|
{ formula: '=ЕСЛИ(A1<>0, "Не ноль", "Ноль")', result: '"Не ноль" если A1 не равно 0' },
|
||||||
|
{ formula: '=ЕСЛИ(И(A1>=0, A1<=10), "В диапазоне", "Вне")', result: 'Проверка попадания в диапазон 0–10' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'И',
|
||||||
|
category: 'Логические',
|
||||||
|
description: 'Возвращает ИСТИНА, если все аргументы истинны.',
|
||||||
|
syntax: 'И(условие1,условие2,...)',
|
||||||
|
args: [
|
||||||
|
{ name: 'условие1,условие2,...', description: 'Логические выражения' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=И(БОЛЬШЕ(5;3),МЕНЬШЕ(2;4))', result: 'ИСТИНА' },
|
||||||
|
{ formula: '=ЕСЛИ(И(A1>0,A1<100),"ОК","Ошибка")', result: '"ОК" если 0 < A1 < 100' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ИЛИ',
|
||||||
|
category: 'Логические',
|
||||||
|
description: 'Возвращает ИСТИНА, если хотя бы один аргумент истинен.',
|
||||||
|
syntax: 'ИЛИ(условие1,условие2,...)',
|
||||||
|
args: [
|
||||||
|
{ name: 'условие1,условие2,...', description: 'Логические выражения' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=ИЛИ(БОЛЬШЕ(1;5),БОЛЬШЕ(10;5))', result: 'ИСТИНА' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'НЕ',
|
||||||
|
category: 'Логические',
|
||||||
|
description: 'Инвертирует логическое значение.',
|
||||||
|
syntax: 'НЕ(значение)',
|
||||||
|
args: [
|
||||||
|
{ name: 'значение', description: 'Логическое значение для инверсии' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=НЕ(ИСТИНА)', result: 'ЛОЖЬ' },
|
||||||
|
{ formula: '=НЕ(БОЛЬШЕ(1;5))', result: 'ИСТИНА' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Сравнение
|
||||||
|
{
|
||||||
|
name: 'БОЛЬШЕ',
|
||||||
|
category: 'Сравнение',
|
||||||
|
description: 'Проверяет, больше ли первое число второго.',
|
||||||
|
syntax: 'БОЛЬШЕ(число1,число2)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число1', description: 'Первое число' },
|
||||||
|
{ name: 'число2', description: 'Второе число' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=БОЛЬШЕ(10,5)', result: 'ИСТИНА' },
|
||||||
|
{ formula: '=БОЛЬШЕ(3,7)', result: 'ЛОЖЬ' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'МЕНЬШЕ',
|
||||||
|
category: 'Сравнение',
|
||||||
|
description: 'Проверяет, меньше ли первое число второго.',
|
||||||
|
syntax: 'МЕНЬШЕ(число1,число2)',
|
||||||
|
args: [
|
||||||
|
{ name: 'число1', description: 'Первое число' },
|
||||||
|
{ name: 'число2', description: 'Второе число' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=МЕНЬШЕ(3,7)', result: 'ИСТИНА' },
|
||||||
|
{ formula: '=МЕНЬШЕ(10,5)', result: 'ЛОЖЬ' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ЕПУСТО',
|
||||||
|
category: 'Сравнение',
|
||||||
|
description: 'Проверяет, является ли значение пустым (null, undefined или пустая строка).',
|
||||||
|
syntax: 'ЕПУСТО(значение)',
|
||||||
|
args: [
|
||||||
|
{ name: 'значение', description: 'Значение для проверки' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=ЕПУСТО(A1)', result: 'ИСТИНА если A1 пуста' },
|
||||||
|
{ formula: '=ЕСЛИ(ЕПУСТО(A1),"Пусто",A1)', result: 'Значение A1 или "Пусто"' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Текстовые
|
||||||
|
{
|
||||||
|
name: 'СЦЕПИТЬ',
|
||||||
|
category: 'Текстовые',
|
||||||
|
description: 'Объединяет несколько текстовых строк в одну.',
|
||||||
|
syntax: 'СЦЕПИТЬ(текст1,текст2,...)',
|
||||||
|
args: [
|
||||||
|
{ name: 'текст1,текст2,...', description: 'Строки для объединения' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=СЦЕПИТЬ("Привет"," ","мир")', result: '"Привет мир"' },
|
||||||
|
{ formula: '=СЦЕПИТЬ(A1," - ",B1)', result: 'Значения A1 и B1 через дефис' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ДЛСТР',
|
||||||
|
category: 'Текстовые',
|
||||||
|
description: 'Возвращает длину текстовой строки.',
|
||||||
|
syntax: 'ДЛСТР(текст)',
|
||||||
|
args: [
|
||||||
|
{ name: 'текст', description: 'Строка для подсчёта длины' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=ДЛСТР("Привет")', result: '6' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ПРОПИСН',
|
||||||
|
category: 'Текстовые',
|
||||||
|
description: 'Преобразует текст в верхний регистр.',
|
||||||
|
syntax: 'ПРОПИСН(текст)',
|
||||||
|
args: [
|
||||||
|
{ name: 'текст', description: 'Строка для преобразования' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=ПРОПИСН("привет")', result: '"ПРИВЕТ"' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'СТРОЧН',
|
||||||
|
category: 'Текстовые',
|
||||||
|
description: 'Преобразует текст в нижний регистр.',
|
||||||
|
syntax: 'СТРОЧН(текст)',
|
||||||
|
args: [
|
||||||
|
{ name: 'текст', description: 'Строка для преобразования' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=СТРОЧН("ПРИВЕТ")', result: '"привет"' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'СЖПРОБЕЛЫ',
|
||||||
|
category: 'Текстовые',
|
||||||
|
description: 'Удаляет начальные и конечные пробелы из текста.',
|
||||||
|
syntax: 'СЖПРОБЕЛЫ(текст)',
|
||||||
|
args: [
|
||||||
|
{ name: 'текст', description: 'Строка для обработки' },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
{ formula: '=СЖПРОБЕЛЫ(" Привет ")', result: '"Привет"' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const CATEGORY_ICONS: Record<string, FC<{ className?: string }>> = {
|
||||||
|
'Случайные числа': Hash,
|
||||||
|
'Математические': Calculator,
|
||||||
|
'Округление': Sigma,
|
||||||
|
'Логические': BookOpen,
|
||||||
|
'Сравнение': Search,
|
||||||
|
'Текстовые': Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CATEGORY_ORDER = [
|
||||||
|
'Математические',
|
||||||
|
'Округление',
|
||||||
|
'Случайные числа',
|
||||||
|
'Логические',
|
||||||
|
'Сравнение',
|
||||||
|
'Текстовые',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const FunctionsWikiPage: FC = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [activeCategory, setActiveCategory] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const q = search.toLowerCase().trim()
|
||||||
|
return FUNCTION_CATALOG.filter(fn => {
|
||||||
|
const matchesSearch = !q || fn.name.toLowerCase().includes(q) || fn.description.toLowerCase().includes(q)
|
||||||
|
const matchesCategory = !activeCategory || fn.category === activeCategory
|
||||||
|
return matchesSearch && matchesCategory
|
||||||
|
})
|
||||||
|
}, [search, activeCategory])
|
||||||
|
|
||||||
|
const grouped = useMemo(() => {
|
||||||
|
const map = new Map<string, FunctionInfo[]>()
|
||||||
|
for (const fn of filtered) {
|
||||||
|
const list = map.get(fn.category) || []
|
||||||
|
list.push(fn)
|
||||||
|
map.set(fn.category, list)
|
||||||
|
}
|
||||||
|
return CATEGORY_ORDER
|
||||||
|
.filter(cat => map.has(cat))
|
||||||
|
.map(cat => ({ category: cat, functions: map.get(cat)! }))
|
||||||
|
}, [filtered])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="sticky top-0 z-10 border-b bg-white shadow-sm">
|
||||||
|
<div className="mx-auto max-w-5xl px-4 py-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
className="rounded-md p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-lg font-semibold text-gray-900">Справочник функций</h1>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{FUNCTION_CATALOG.length} функций в {CATEGORY_ORDER.length} категориях
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto max-w-5xl px-4 py-6">
|
||||||
|
{/* Operators reference */}
|
||||||
|
<div className="mb-6 rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||||
|
<h3 className="mb-2 text-sm font-semibold text-blue-800">Операторы сравнения (как в Excel)</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm sm:grid-cols-3">
|
||||||
|
<div><code className="font-medium text-blue-700">=</code> <span className="text-gray-600">равно</span></div>
|
||||||
|
<div><code className="font-medium text-blue-700">{'<>'}</code> <span className="text-gray-600">не равно</span></div>
|
||||||
|
<div><code className="font-medium text-blue-700">{'>'}</code> <span className="text-gray-600">больше</span></div>
|
||||||
|
<div><code className="font-medium text-blue-700">{'<'}</code> <span className="text-gray-600">меньше</span></div>
|
||||||
|
<div><code className="font-medium text-blue-700">{'>='}</code> <span className="text-gray-600">больше или равно</span></div>
|
||||||
|
<div><code className="font-medium text-blue-700">{'<='}</code> <span className="text-gray-600">меньше или равно</span></div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-xs text-gray-500">Пример: <code className="text-blue-700">=ЕСЛИ(A1{'<>'}0, "Не ноль", "Ноль")</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search & Filters */}
|
||||||
|
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Поиск функций..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
className="w-full rounded-lg border border-gray-300 bg-white py-2 pl-10 pr-4 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveCategory(null)}
|
||||||
|
className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${
|
||||||
|
!activeCategory
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Все
|
||||||
|
</button>
|
||||||
|
{CATEGORY_ORDER.map(cat => {
|
||||||
|
const Icon = CATEGORY_ICONS[cat]
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={cat}
|
||||||
|
onClick={() => setActiveCategory(activeCategory === cat ? null : cat)}
|
||||||
|
className={`flex items-center gap-1 rounded-full px-3 py-1 text-xs font-medium transition-colors ${
|
||||||
|
activeCategory === cat
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{Icon && <Icon className="h-3 w-3" />}
|
||||||
|
{cat}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results */}
|
||||||
|
{grouped.length === 0 ? (
|
||||||
|
<div className="py-12 text-center text-gray-500">
|
||||||
|
Функции не найдены. Попробуйте изменить запрос.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{grouped.map(({ category, functions }) => {
|
||||||
|
const Icon = CATEGORY_ICONS[category]
|
||||||
|
return (
|
||||||
|
<section key={category}>
|
||||||
|
<div className="mb-3 flex items-center gap-2">
|
||||||
|
{Icon && <Icon className="h-5 w-5 text-gray-400" />}
|
||||||
|
<h2 className="text-base font-semibold text-gray-800">{category}</h2>
|
||||||
|
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-500">
|
||||||
|
{functions.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
{functions.map(fn => (
|
||||||
|
<FunctionCard key={fn.name} fn={fn} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FunctionCard: FC<{ fn: FunctionInfo }> = ({ fn }) => {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-shadow hover:shadow-md">
|
||||||
|
<div className="mb-2 flex items-start justify-between gap-2">
|
||||||
|
<code className="rounded bg-blue-50 px-2 py-0.5 text-sm font-bold text-blue-700">
|
||||||
|
{fn.name}
|
||||||
|
</code>
|
||||||
|
{fn.notes && (
|
||||||
|
<span className="shrink-0 rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-medium text-amber-600">
|
||||||
|
{fn.notes.length > 30 ? fn.notes.slice(0, 30) + '...' : fn.notes}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mb-2 text-sm text-gray-600">{fn.description}</p>
|
||||||
|
|
||||||
|
{/* Syntax */}
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="mb-1 text-[11px] font-medium uppercase tracking-wide text-gray-400">
|
||||||
|
Синтаксис
|
||||||
|
</div>
|
||||||
|
<code className="block rounded bg-gray-50 px-2 py-1 text-xs text-gray-700">
|
||||||
|
={fn.syntax}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Args */}
|
||||||
|
{fn.args.length > 0 && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="mb-1 text-[11px] font-medium uppercase tracking-wide text-gray-400">
|
||||||
|
Аргументы
|
||||||
|
</div>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{fn.args.map(arg => (
|
||||||
|
<div key={arg.name} className="flex gap-1.5 text-xs">
|
||||||
|
<code className="shrink-0 font-medium text-gray-700">{arg.name}</code>
|
||||||
|
{arg.optional && (
|
||||||
|
<span className="shrink-0 text-[10px] text-gray-400">(необяз.)</span>
|
||||||
|
)}
|
||||||
|
<span className="text-gray-500">— {arg.description}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Examples */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 text-[11px] font-medium uppercase tracking-wide text-gray-400">
|
||||||
|
Примеры
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{fn.examples.map((ex, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-2 text-xs">
|
||||||
|
<code className="rounded bg-green-50 px-1.5 py-0.5 text-green-700">{ex.formula}</code>
|
||||||
|
<span className="text-gray-400">→</span>
|
||||||
|
<span className="text-gray-600">{ex.result}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FunctionsWikiPage } from './functions-wiki/Page'
|
||||||
import { StandardsManagementPage } from './standards-management/Page'
|
import { StandardsManagementPage } from './standards-management/Page'
|
||||||
import { TemplateEditPage } from './templates/edit/Page'
|
import { TemplateEditPage } from './templates/edit/Page'
|
||||||
import { ElementsCreation } from './templates/elements/Page'
|
import { ElementsCreation } from './templates/elements/Page'
|
||||||
@@ -6,6 +7,7 @@ import { ProtocolCreation } from './templates/protocols/Page'
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
ElementsCreation,
|
ElementsCreation,
|
||||||
|
FunctionsWikiPage,
|
||||||
ProtocolCreation,
|
ProtocolCreation,
|
||||||
StandardsManagementPage,
|
StandardsManagementPage,
|
||||||
TemplateEditPage,
|
TemplateEditPage,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
import { arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
import { arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {
|
import {
|
||||||
|
BookOpen,
|
||||||
CheckSquare,
|
CheckSquare,
|
||||||
Copy,
|
Copy,
|
||||||
Edit3,
|
Edit3,
|
||||||
@@ -49,6 +50,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useCallback, useMemo, useState } from 'react'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Button } from 'shared/ui/button'
|
import { Button } from 'shared/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from 'shared/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from 'shared/ui/card'
|
||||||
import { Checkbox } from 'shared/ui/checkbox'
|
import { Checkbox } from 'shared/ui/checkbox'
|
||||||
@@ -346,6 +348,7 @@ const TemplateForm: React.FC<TemplateFormProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TemplatesPageContent = () => {
|
const TemplatesPageContent = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
const {
|
const {
|
||||||
templates,
|
templates,
|
||||||
deleteTemplate,
|
deleteTemplate,
|
||||||
@@ -935,6 +938,15 @@ const TemplatesPageContent = () => {
|
|||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||||
<h1 className="text-lg font-semibold">Карточки протоколов</h1>
|
<h1 className="text-lg font-semibold">Карточки протоколов</h1>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => navigate('/functions-wiki')}
|
||||||
|
className="ml-2"
|
||||||
|
>
|
||||||
|
<BookOpen className="mr-1 h-4 w-4" />
|
||||||
|
Справочник функций
|
||||||
|
</Button>
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
{/* Кнопка сохранения порядка */}
|
{/* Кнопка сохранения порядка */}
|
||||||
|
|||||||
@@ -14,16 +14,24 @@
|
|||||||
"outDir": "./dist/",
|
"outDir": "./dist/",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": [
|
||||||
"shared/*": ["./shared/*"]
|
"./src/*"
|
||||||
|
],
|
||||||
|
"shared/*": [
|
||||||
|
"./shared/*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
|
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "CommonJS"
|
"module": "CommonJS"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "dist", "build", "coverage"]
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"build",
|
||||||
|
"coverage"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user