Init base project files
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
64
.gitignore
vendored
Normal file
64
.gitignore
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
# ---> Java
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
*.gz
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
# Vue: gitignore template for Vue.js projects
|
||||
|
||||
# Recommended template: Node.gitignore
|
||||
|
||||
# Root
|
||||
/upload/
|
||||
/.idea
|
||||
*.iml
|
||||
|
||||
# Backnend module
|
||||
/backend/app/src/main/resources/application-dev.yaml
|
||||
/backend/app/src/main/resources/public/
|
||||
/backend/app/upload/
|
||||
/backend/**/target/
|
||||
|
||||
# Frontend module
|
||||
/frontend/dist
|
||||
/frontend/upload
|
||||
/frontend/node_modules/*
|
||||
/frontend/node
|
||||
/frontend/**/target/
|
||||
/frontend/components.d.ts
|
||||
/frontend/tsconfig.app.tsbuildinfo
|
||||
/frontend/.vite
|
||||
/frontend/.turbo
|
||||
|
||||
# Файлы сборки/компиляции для модулей
|
||||
/frontend/src/modules/*/dist/
|
||||
/frontend/src/modules/*/node_modules/
|
||||
/frontend/src/modules/*.tsbuildinfo
|
||||
/frontend/tsconfig.tsbuildinfo
|
||||
|
||||
# Игнорим lock-файлы всех модулей
|
||||
/frontend**/package-lock.json
|
||||
|
||||
# Исключаем лок-файл самого приложения
|
||||
!/frontend/package-lock.json
|
||||
3
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
3
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM eclipse-temurin:25.0.2_10-jre-alpine-3.23
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ARG PROFILE
|
||||
ARG PORT
|
||||
ARG DB_HOST
|
||||
ARG DB_PASSWORD
|
||||
|
||||
ENV PROFILE $PROFILE
|
||||
ENV PORT $PORT
|
||||
ENV DB_HOST $DB_HOST
|
||||
ENV DB_PASSWORD $DB_PASSWORD
|
||||
|
||||
COPY backend/app/target/app-1.0.0.jar /app/backend.jar
|
||||
|
||||
ENTRYPOINT ["java","-jar","/app/backend.jar"]
|
||||
@@ -1,3 +0,0 @@
|
||||
# supp-forms-based-app
|
||||
|
||||
Базовый репозиторий для форка новых проектов, базирующихся на supp-forms
|
||||
150
backend/app/pom.xml
Normal file
150
backend/app/pom.xml
Normal file
@@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>app</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<parent>
|
||||
<groupId>org.itprom</groupId>
|
||||
<artifactId>backend</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<name>Sergey Verbitsky</name>
|
||||
<organization>It-prom</organization>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<properties>
|
||||
<skip.deploy>true</skip.deploy>
|
||||
|
||||
<maven.compiler.source>25</maven.compiler.source>
|
||||
<maven.compiler.target>25</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.itprom</groupId>
|
||||
<artifactId>backend</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- DB -->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Supp dependencies -->
|
||||
|
||||
<!-- Base config -->
|
||||
<dependency>
|
||||
<groupId>org.itprom</groupId>
|
||||
<artifactId>base-config</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Core -->
|
||||
<dependency>
|
||||
<groupId>org.itprom</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Log -->
|
||||
<dependency>
|
||||
<groupId>org.itprom</groupId>
|
||||
<artifactId>log</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Menu -->
|
||||
<dependency>
|
||||
<groupId>org.itprom</groupId>
|
||||
<artifactId>menu</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Localization -->
|
||||
<dependency>
|
||||
<groupId>org.itprom</groupId>
|
||||
<artifactId>l10n</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Notification -->
|
||||
<dependency>
|
||||
<groupId>org.itprom</groupId>
|
||||
<artifactId>notification</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>4.0.3</version>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>3.4.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy Vue.js frontend content</id>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>src/main/resources/public</outputDirectory>
|
||||
<overwrite>true</overwrite>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>../../frontend/target/dist</directory>
|
||||
<includes>
|
||||
<include>index.html</include>
|
||||
<include>favicon.ico</include>
|
||||
<include>company_logo.png</include>
|
||||
<include>static/**</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.itprom.suppforms;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.persistence.autoconfigure.EntityScan;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
/**
|
||||
* Точка входа в основное приложение.
|
||||
*
|
||||
* @author Sergey Verbitsky
|
||||
* @since 15.12.2025
|
||||
*/
|
||||
|
||||
@ConfigurationPropertiesScan
|
||||
@EnableConfigurationProperties
|
||||
@SpringBootApplication
|
||||
@EnableJpaRepositories("org.itprom")
|
||||
@EntityScan("org.itprom")
|
||||
@ComponentScan("org.itprom")
|
||||
public class SuppFormsApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SuppFormsApplication.class, args);
|
||||
}
|
||||
}
|
||||
59
backend/app/src/main/resources/application.yaml
Normal file
59
backend/app/src/main/resources/application.yaml
Normal file
@@ -0,0 +1,59 @@
|
||||
server:
|
||||
port: 9001
|
||||
spring:
|
||||
profiles:
|
||||
active: ${PROFILE:dev}
|
||||
servlet:
|
||||
multipart:
|
||||
enabled: true
|
||||
max-file-size: 10MB
|
||||
max-request-size: 15MB
|
||||
file-size-threshold: 2KB
|
||||
sql:
|
||||
init:
|
||||
mode: always
|
||||
schema-locations:
|
||||
- classpath*:db/**/*.sql
|
||||
jpa:
|
||||
defer-datasource-initialization: true
|
||||
mvc:
|
||||
pathmatch:
|
||||
matching-strategy: ant_path_matcher
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
swagger-ui:
|
||||
url: /api-docs
|
||||
jwt:
|
||||
private.key: classpath:/jwk/supp-5.key
|
||||
public.key: classpath:/jwk/supp-5.pub
|
||||
expiration: 86400
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health, info, metrics, prometheus
|
||||
metrics:
|
||||
distribution:
|
||||
percentiles-histogram:
|
||||
http.server.requests: true
|
||||
tags:
|
||||
application: supp-forms
|
||||
|
||||
#- SUPP ------------------------------------------------------
|
||||
supp:
|
||||
defaultRoles:
|
||||
- name: Administrator
|
||||
- name: SuppUser
|
||||
pages:
|
||||
- num: 2
|
||||
access: [ read, write ]
|
||||
enablePathRegistryLog: false
|
||||
websocket:
|
||||
allowedOrigins:
|
||||
- "http://localhost:9001"
|
||||
- "http://localhost:5173"
|
||||
- "http://127.0.0.1:5173"
|
||||
file:
|
||||
upload-dir: ./upload
|
||||
139
backend/pom.xml
Normal file
139
backend/pom.xml
Normal file
@@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.itprom</groupId>
|
||||
<artifactId>supp-forms-based-app</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>backend</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>backend</name>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<name>Sergey Verbitsky</name>
|
||||
<organization>It-prom</organization>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<modules>
|
||||
<module>app</module>
|
||||
<!-- Add custom new modules here -->
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<maven.deploy.skip>true</maven.deploy.skip>
|
||||
|
||||
<java.version>25</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<spring.boot.version>4.0.3</spring.boot.version>
|
||||
<lombok.version>1.18.42</lombok.version>
|
||||
<commons-lang3.version>3.20.0</commons-lang3.version>
|
||||
<jsoup.version>1.21.2</jsoup.version>
|
||||
<swagger-annotations.version>2.2.23</swagger-annotations.version>
|
||||
<aspectj.version>1.9.23</aspectj.version>
|
||||
<postgresql.version>42.7.8</postgresql.version>
|
||||
<h2.version>2.2.224</h2.version>
|
||||
<springdoc.version>3.0.0</springdoc.version>
|
||||
<commons-io.version>2.14.0</commons-io.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- Spring Boot BOM -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Common dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>${commons-lang3.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>${jsoup.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
<version>${swagger-annotations.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.aspectj</groupId>
|
||||
<artifactId>aspectjrt</artifactId>
|
||||
<version>${aspectj.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.aspectj</groupId>
|
||||
<artifactId>aspectjweaver</artifactId>
|
||||
<version>${aspectj.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons-io.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- DB -->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>${postgresql.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Springdoc -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>${springdoc.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
<parameters>true</parameters>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.42</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
</project>
|
||||
1
frontend/env.d.ts
vendored
Normal file
1
frontend/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
95
frontend/eslint.config.js
Normal file
95
frontend/eslint.config.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import globals from 'globals';
|
||||
import js from '@eslint/js';
|
||||
|
||||
export default [
|
||||
// Игнорируемые файлы и папки
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules/**', // пакеты
|
||||
'**/dist/**', // сборка
|
||||
'**/build/**', // сборка
|
||||
'**/*.d.ts', // типы TS
|
||||
'**/*.d.ts.map' // sourcemap типовых файлов
|
||||
]
|
||||
},
|
||||
// Конфиг для JS файлов
|
||||
{
|
||||
files: ['**/*.{js,jsx}'], // проверяем только JS/JSX
|
||||
...js.configs.recommended, // базовые правила ESLint
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest', // современный JS
|
||||
sourceType: 'module', // ES модули
|
||||
globals: { // глобальные переменные
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-console': 'off', // консоль разрешена
|
||||
'no-debugger': process.env.NODE_ENV === 'production'
|
||||
? 'error'
|
||||
: 'warn', // debugger в dev — warn, в prod — error
|
||||
'semi': ['error', 'always'], // точка с запятой обязательна
|
||||
'quotes': ['error', 'single', { avoidEscape: true }] // одинарные кавычки, экранирование разрешено
|
||||
}
|
||||
},
|
||||
// Конфиг для TypeScript файлов
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
parser: await import('@typescript-eslint/parser').then(m => m.default),
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': await import('@typescript-eslint/eslint-plugin').then(m => m.default)
|
||||
},
|
||||
rules: {
|
||||
'no-console': 'off', // консоль разрешена
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
|
||||
'semi': ['error', 'always'], // обязательные точки с запятой
|
||||
'quotes': ['error', 'single', {
|
||||
avoidEscape: true,
|
||||
allowTemplateLiterals: true
|
||||
}], // одинарные кавычки, шаблонные строки разрешены
|
||||
'no-unused-vars': 'off', // TS не ругается на неиспользуемые переменные
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off' // any разрешен
|
||||
}
|
||||
},
|
||||
// Конфиг для Vue файлов
|
||||
{
|
||||
files: ['**/*.vue'], // проверка только Vue
|
||||
languageOptions: {
|
||||
parser: await import('vue-eslint-parser').then(m => m.default),
|
||||
parserOptions: {
|
||||
parser: await import('@typescript-eslint/parser').then(m => m.default), // TS внутри <script lang="ts">
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
extraFileExtensions: ['.vue'], // чтение Vue файлов
|
||||
jsx: true // поддержка JSX внутри Vue (если используется)
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'vue': await import('eslint-plugin-vue').then(m => m.default)
|
||||
},
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
|
||||
'semi': ['error', 'always'],
|
||||
'quotes': ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }],
|
||||
'vue/multi-word-component-names': 'off', // разрешить односоставные имена компонентов
|
||||
'vue/jsx-uses-vars': 'error' // фикс для JSX в шаблонах Vue
|
||||
}
|
||||
}
|
||||
];
|
||||
16
frontend/index.html
Normal file
16
frontend/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap&subset=latin,cyrillic" rel="stylesheet">
|
||||
<title>СУПП-Формы</title>
|
||||
</head>
|
||||
<body style="margin: 0">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
8169
frontend/package-lock.json
generated
Normal file
8169
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
76
frontend/package.json
Normal file
76
frontend/package.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "npm@11.6.2",
|
||||
"workspaces": [
|
||||
"src/modules/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build-css": "node src/scripts/build-itprom-css.js",
|
||||
"predev": "npm run build-css",
|
||||
"dev": "vite",
|
||||
"prebuild": "npm run build-css",
|
||||
"build": "turbo run build && vue-tsc -b && vite build",
|
||||
"lint": "turbo run lint",
|
||||
"add-module": "node scripts/add-module.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@itprom/core": "^1.0.0",
|
||||
"@itprom/core-components": "^1.0.0",
|
||||
"@itprom/department": "^1.0.0",
|
||||
"@itprom/favorites": "^1.0.0",
|
||||
"@itprom/language": "^1.0.0",
|
||||
"@itprom/menu": "^1.0.0",
|
||||
"@itprom/message": "^1.0.0",
|
||||
"@itprom/notification": "^1.0.0",
|
||||
"@itprom/page": "^1.0.0",
|
||||
"@itprom/person": "^1.0.0",
|
||||
"@itprom/place": "^1.0.0",
|
||||
"@itprom/role": "^1.0.0",
|
||||
"@itprom/settings": "^1.0.0",
|
||||
"@itprom/tablelayout": "^1.0.0",
|
||||
"@itprom/user": "^1.0.0",
|
||||
"@itprom/user-profile": "^1.0.0",
|
||||
"@itprom/help": "^1.0.0",
|
||||
"@noble/ciphers": "^2.1.1",
|
||||
"@stomp/stompjs": "^7.2.1",
|
||||
"@vueup/vue-quill": "^1.2.0",
|
||||
"apexcharts": "^5.3.6",
|
||||
"axios": "^1.13.5",
|
||||
"dayjs": "^1.11.19",
|
||||
"element-plus": "^2.13.2",
|
||||
"eslint-define-config": "^2.1.0",
|
||||
"exceljs": "^4.4.0",
|
||||
"glob": "^13.0.0",
|
||||
"pinia": "^3.0.4",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sockjs": "^0.3.24",
|
||||
"sockjs-client": "^1.6.1",
|
||||
"vue": "^3.5.29",
|
||||
"vue-draggable-next": "^2.3.0",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^5.0.2",
|
||||
"vue3-apexcharts": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^8.54.0",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.0",
|
||||
"@vue/eslint-config-typescript": "^14.6.0",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-plugin-vue": "^10.5.1",
|
||||
"jiti": "^2.6.1",
|
||||
"sass-embedded": "^1.97.3",
|
||||
"turbo": "^2.8.12",
|
||||
"typescript": "~5.9.3",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-checker": "^0.12.0",
|
||||
"vue-tsc": "^3.1.5"
|
||||
}
|
||||
}
|
||||
65
frontend/pom.xml
Normal file
65
frontend/pom.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.itprom</groupId>
|
||||
<artifactId>frontend</artifactId>
|
||||
<version>0.0.1</version>
|
||||
|
||||
<properties>
|
||||
<maven.deploy.skip>true</maven.deploy.skip>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<frontend-maven-plugin.version>1.15.4</frontend-maven-plugin.version>
|
||||
<node.version>v24.11.1</node.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.github.eirslett</groupId>
|
||||
<artifactId>frontend-maven-plugin</artifactId>
|
||||
<version>${frontend-maven-plugin.version}</version>
|
||||
<executions>
|
||||
<!-- Install node and npm -->
|
||||
<execution>
|
||||
<id>install node and npm</id>
|
||||
<goals>
|
||||
<goal>install-node-and-npm</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<nodeVersion>${node.version}</nodeVersion>
|
||||
</configuration>
|
||||
</execution>
|
||||
<!-- Install all project dependencies -->
|
||||
<execution>
|
||||
<id>npm install</id>
|
||||
<goals>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<phase>generate-resources</phase>
|
||||
<configuration>
|
||||
<arguments>install</arguments>
|
||||
<workingDirectory>${project.basedir}</workingDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
<!-- Build and minify static files -->
|
||||
<execution>
|
||||
<id>npm run build</id>
|
||||
<goals>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<phase>generate-resources</phase>
|
||||
<configuration>
|
||||
<arguments>run build</arguments>
|
||||
<workingDirectory>${project.basedir}</workingDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
247
frontend/scripts/add-module.js
Normal file
247
frontend/scripts/add-module.js
Normal file
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function getArg(name) {
|
||||
const idx = args.indexOf(name);
|
||||
return idx !== -1 ? args[idx + 1] : null;
|
||||
}
|
||||
|
||||
function toKebab(str) {
|
||||
return str
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
||||
.replace(/_/g, '-')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
const moduleName = getArg('-name');
|
||||
const moduleNum = getArg('-num');
|
||||
|
||||
if (!moduleName || !moduleNum) {
|
||||
console.log('Используй: npm run add-module -- -name settings -num 10');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const root = process.cwd();
|
||||
const folderName = toKebab(moduleName);
|
||||
const modulesDir = path.join(root, 'src/modules');
|
||||
const moduleDir = path.join(modulesDir, folderName);
|
||||
|
||||
if (fs.existsSync(moduleDir)) {
|
||||
console.log('Модуль уже существует');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pascal =
|
||||
moduleName
|
||||
.replace(/(^\w|[A-Z]|\b\w)/g, c => c.toUpperCase())
|
||||
.replace(/[-_]/g, '');
|
||||
|
||||
// ---------- helpers ----------
|
||||
const EOL = '\n';
|
||||
|
||||
function write(file, content) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, content.trimStart().replace(/\r?\n/g, EOL));
|
||||
}
|
||||
|
||||
// ---------- configs ----------
|
||||
const viteConfig = `
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import path from 'node:path';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'src/index.ts'),
|
||||
name: '${pascal}',
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => \`index.\${format}.js\`
|
||||
},
|
||||
rollupOptions: {
|
||||
external: (id) => id.startsWith('@itprom/') || ['vue', 'pinia', 'vue-router', 'element-plus'].includes(id),
|
||||
output: {
|
||||
preserveModules: false
|
||||
}
|
||||
},
|
||||
outDir: 'dist'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '../../')
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx()
|
||||
]
|
||||
});
|
||||
`;
|
||||
|
||||
const tsconfig = `
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"verbatimModuleSyntax": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "vue",
|
||||
"types": [
|
||||
"vite/client",
|
||||
"node"
|
||||
],
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.vue",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
const tsconfigBuild = `
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "dist",
|
||||
"noEmit": false,
|
||||
"paths": {
|
||||
"@itprom/*": [
|
||||
"../src/modules/*/src/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
const pkg = `
|
||||
{
|
||||
"name": "@itprom/${folderName}",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.es.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "vite build && vue-tsc -p tsconfig.build.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@itprom/core": "^1.0.0",
|
||||
"@itprom/core-components": "^1.0.0",
|
||||
"vue": "^3.5.27"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.es.js",
|
||||
"require": "./dist/index.cjs.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ---------- src files ----------
|
||||
|
||||
const moduleInfo = `
|
||||
import { type ModuleInfo } from '@itprom/core';
|
||||
|
||||
export const moduleInfo: ModuleInfo = {
|
||||
name: '${moduleName}',
|
||||
num: '${moduleNum}',
|
||||
component: () => import('./components/${pascal}Component.vue')
|
||||
};
|
||||
`;
|
||||
|
||||
const indexTs = `
|
||||
export * from './classes/${pascal}';
|
||||
export { } from './classes/${pascal}Service';
|
||||
export { default as ${pascal}Component } from './components/${pascal}Component.vue';
|
||||
export { moduleInfo } from './moduleInfo';
|
||||
`;
|
||||
|
||||
const componentVue = `
|
||||
<template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
`;
|
||||
|
||||
const classTs = `
|
||||
export class ${pascal} {
|
||||
|
||||
}
|
||||
`;
|
||||
|
||||
const serviceTs = `
|
||||
export class ${pascal}Service {
|
||||
|
||||
}
|
||||
`;
|
||||
|
||||
// ---------- write ----------
|
||||
|
||||
write(path.join(moduleDir, 'vite.config.ts'), viteConfig);
|
||||
write(path.join(moduleDir, 'tsconfig.json'), tsconfig);
|
||||
write(path.join(moduleDir, 'tsconfig.build.json'), tsconfigBuild);
|
||||
write(path.join(moduleDir, 'package.json'), pkg);
|
||||
|
||||
write(path.join(moduleDir, 'src/components', `${pascal}Component.vue`), componentVue);
|
||||
write(path.join(moduleDir, 'src/classes', `${pascal}.ts`), classTs);
|
||||
write(path.join(moduleDir, 'src/classes', `${pascal}Service.ts`), serviceTs);
|
||||
write(path.join(moduleDir, 'src/moduleInfo.ts'), moduleInfo);
|
||||
write(path.join(moduleDir, 'src/index.ts'), indexTs);
|
||||
|
||||
// ---------- update modules.json ----------
|
||||
const modulesJsonPath = path.join(root, 'scripts/modules.json');
|
||||
|
||||
if (fs.existsSync(modulesJsonPath)) {
|
||||
const list = JSON.parse(fs.readFileSync(modulesJsonPath, 'utf8'));
|
||||
|
||||
if (!list.includes(folderName)) {
|
||||
list.push(folderName);
|
||||
fs.writeFileSync(modulesJsonPath, JSON.stringify(list, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- update root package.json ----------
|
||||
const rootPkgPath = path.join(root, 'package.json');
|
||||
|
||||
if (fs.existsSync(rootPkgPath)) {
|
||||
const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, 'utf8'));
|
||||
|
||||
rootPkg.dependencies ??= {};
|
||||
|
||||
const depName = `@itprom/${folderName}`;
|
||||
|
||||
if (!rootPkg.dependencies[depName]) {
|
||||
rootPkg.dependencies[depName] = 'workspace:*';
|
||||
}
|
||||
|
||||
fs.writeFileSync(rootPkgPath, JSON.stringify(rootPkg, null, 2));
|
||||
}
|
||||
|
||||
console.log(`✅ Модуль ${moduleName} создан`);
|
||||
28
frontend/scripts/modules-build.ps1
Normal file
28
frontend/scripts/modules-build.ps1
Normal file
@@ -0,0 +1,28 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$REGISTRY = $env:NEXUS_REGISTRY_NPM
|
||||
if (-not $REGISTRY) { $REGISTRY = "http://localhost:8081/repository/local-npm/" }
|
||||
|
||||
# Важно!
|
||||
# Модули перечисляются в порядке, который учитывает зависимость данного модуля от остальных.
|
||||
# Например: core - первый в списке, т.к. от него зависят все остальные модули, модуль role идет после core и page - т.к.
|
||||
# он зависит от них.
|
||||
|
||||
$MODULES_DIR = "..\src\modules"
|
||||
$modules = Get-Content .\modules.json | ConvertFrom-Json
|
||||
|
||||
foreach ($modName in $modules) {
|
||||
$mod = Join-Path $MODULES_DIR $modName
|
||||
Write-Host "=== Build $modName ==="
|
||||
|
||||
# Очистка dist
|
||||
$dist = Join-Path $mod "dist"
|
||||
if (Test-Path $dist) { Remove-Item -Recurse -Force $dist }
|
||||
|
||||
# Билд
|
||||
Push-Location $mod
|
||||
npm run build
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Write-Host "Building completed."
|
||||
27
frontend/scripts/modules-build.sh
Normal file
27
frontend/scripts/modules-build.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
MODULES_DIR="../src/modules"
|
||||
|
||||
# Важно!
|
||||
# Модули перечисляются в порядке, который учитывает зависимость данного модуля от остальных.
|
||||
# Например: core - первый в списке, т.к. от него зависят все остальные модули, модуль role идет после core и page - т.к.
|
||||
# он зависит от них.
|
||||
|
||||
modules=()
|
||||
while IFS= read -r line; do
|
||||
modules+=("$line")
|
||||
done < <(jq -r '.[]' modules.json)
|
||||
|
||||
for modName in "${modules[@]}"; do
|
||||
mod="$MODULES_DIR/$modName"
|
||||
echo "=== Deploying $modName ==="
|
||||
|
||||
# Очистка dist
|
||||
[ -d "$mod/dist" ] && rm -rf "$mod/dist"
|
||||
|
||||
# Билд
|
||||
(cd "$mod" && npm run build)
|
||||
done
|
||||
|
||||
echo "✅ Deployment completed."
|
||||
33
frontend/scripts/modules-deploy.ps1
Normal file
33
frontend/scripts/modules-deploy.ps1
Normal file
@@ -0,0 +1,33 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$REGISTRY = $env:NEXUS_REGISTRY_NPM
|
||||
if (-not $REGISTRY) { $REGISTRY = "http://localhost:8081/repository/local-npm/" }
|
||||
|
||||
# Важно!
|
||||
# Модули перечисляются в порядке, который учитывает зависимость данного модуля от остальных.
|
||||
# Например: core - первый в списке, т.к. от него зависят все остальные модули, модуль role идет после core и page - т.к.
|
||||
# он зависит от них.
|
||||
|
||||
$MODULES_DIR = "..\src\modules"
|
||||
$modules = Get-Content .\modules.json | ConvertFrom-Json
|
||||
|
||||
foreach ($modName in $modules) {
|
||||
$mod = Join-Path $MODULES_DIR $modName
|
||||
Write-Host "=== Deploying $modName ==="
|
||||
|
||||
# Очистка dist
|
||||
$dist = Join-Path $mod "dist"
|
||||
if (Test-Path $dist) { Remove-Item -Recurse -Force $dist }
|
||||
|
||||
# Билд
|
||||
Push-Location $mod
|
||||
npm run build
|
||||
Pop-Location
|
||||
|
||||
# Публикация
|
||||
Push-Location $mod
|
||||
npm publish --registry $REGISTRY
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Write-Host "✅ Deployment completed."
|
||||
31
frontend/scripts/modules-deploy.sh
Normal file
31
frontend/scripts/modules-deploy.sh
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
REGISTRY="${NEXUS_REGISTRY_NPM:-http://localhost:8081/repository/local-npm/}"
|
||||
MODULES_DIR="../src/modules"
|
||||
|
||||
# Важно!
|
||||
# Модули перечисляются в порядке, который учитывает зависимость данного модуля от остальных.
|
||||
# Например: core - первый в списке, т.к. от него зависят все остальные модули, модуль role идет после core и page - т.к.
|
||||
# он зависит от них.
|
||||
|
||||
modules=()
|
||||
while IFS= read -r line; do
|
||||
modules+=("$line")
|
||||
done < <(jq -r '.[]' modules.json)
|
||||
|
||||
for modName in "${modules[@]}"; do
|
||||
mod="$MODULES_DIR/$modName"
|
||||
echo "=== Deploying $modName ==="
|
||||
|
||||
# Очистка dist
|
||||
[ -d "$mod/dist" ] && rm -rf "$mod/dist"
|
||||
|
||||
# Билд
|
||||
(cd "$mod" && npm run build)
|
||||
|
||||
# Публикация
|
||||
(cd "$mod" && npm publish --registry "$REGISTRY")
|
||||
done
|
||||
|
||||
echo "✅ Deployment completed."
|
||||
22
frontend/scripts/modules.json
Normal file
22
frontend/scripts/modules.json
Normal file
@@ -0,0 +1,22 @@
|
||||
[
|
||||
"core",
|
||||
"core-components",
|
||||
"language",
|
||||
"message",
|
||||
"page",
|
||||
"favorites",
|
||||
"place",
|
||||
"role",
|
||||
"menu",
|
||||
"settings",
|
||||
"profession",
|
||||
"notification",
|
||||
"user",
|
||||
"tablelayout",
|
||||
"user-profile",
|
||||
"department",
|
||||
"shift",
|
||||
"person",
|
||||
"shift-seq",
|
||||
"help"
|
||||
]
|
||||
85
frontend/src/App.vue
Normal file
85
frontend/src/App.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<el-config-provider :locale="locale">
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, onBeforeMount, onMounted, provide, watch } from 'vue';
|
||||
import { dayjs } from 'element-plus';
|
||||
import { useRoute } from 'vue-router';
|
||||
import ru from 'element-plus/es/locale/lang/ru';
|
||||
import en from 'element-plus/es/locale/lang/en';
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css';
|
||||
import { type L10Service, updateMessages, useUserStore, websocketService } from '@itprom/core';
|
||||
import { router, setupAxiosInterceptors, setupBearerTokenHeader } from '@/router/router.ts';
|
||||
|
||||
/** Store */
|
||||
const userStore = useUserStore();
|
||||
|
||||
/** Provide */
|
||||
provide('navigation', {
|
||||
goToLogin() {
|
||||
router.push('/login');
|
||||
},
|
||||
goToRoot() {
|
||||
router.push('/');
|
||||
}
|
||||
});
|
||||
|
||||
/** Model & Injects */
|
||||
const l10Service = inject<L10Service>('l10Service');
|
||||
|
||||
/** Data */
|
||||
const route = useRoute();
|
||||
|
||||
/** Computed */
|
||||
const locale = computed(() => {
|
||||
if (userStore.currentLocale.includes('en')) {
|
||||
dayjs.locale('en');
|
||||
return en;
|
||||
} else if (userStore.currentLocale.includes('ru')) {
|
||||
dayjs.locale('ru');
|
||||
return ru;
|
||||
}
|
||||
});
|
||||
|
||||
/** Configuration */
|
||||
dayjs.locale(locale.value.name);
|
||||
|
||||
|
||||
/** Watch */
|
||||
watch(() => userStore.currentLocale, () => updateMessages(l10Service));
|
||||
|
||||
// Отслеживаем изменение темы, чтобы установить необходимые атрибуты для css
|
||||
watch(() => userStore.theme, (val) => {
|
||||
const isDark = val === 'dark';
|
||||
document.documentElement.dataset.colorScheme = isDark ? 'dark' : 'light';
|
||||
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
watch(() => userStore.token, () => {
|
||||
setupAxiosInterceptors();
|
||||
setupBearerTokenHeader();
|
||||
}, { immediate: true });
|
||||
|
||||
/** Hooks */
|
||||
onBeforeMount(() => {
|
||||
websocketService.activate();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// отслеживаем нажатие кнопки назад: если пользователь выполнил вход,
|
||||
// то не даём перейти ему на страницу входа
|
||||
window.onpopstate = () => {
|
||||
if (userStore.token && route.path === '/login') {
|
||||
router.push('/');
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
426
frontend/src/components/Main.vue
Normal file
426
frontend/src/components/Main.vue
Normal file
@@ -0,0 +1,426 @@
|
||||
<!-- Основное окно программы -->
|
||||
<template>
|
||||
<el-container class="supp-app-container">
|
||||
<!-- Шапка -->
|
||||
<el-header>
|
||||
<Logo type="menu" />
|
||||
<SuppSectionMenu
|
||||
class="section-menu"
|
||||
@section-menu-clicked="onSectionMenuClicked" />
|
||||
<el-switch
|
||||
class="user-theme-switch"
|
||||
inline-prompt
|
||||
:style="switchStyle"
|
||||
:inactive-action-icon="Sunny"
|
||||
:active-action-icon="Moon"
|
||||
:model-value="activeDarkTheme"
|
||||
@change="onThemeChanged" />
|
||||
<SuppUserMenu
|
||||
:places="placeItems"
|
||||
class="user-menu"
|
||||
@logout-menu-clicked="logout"
|
||||
@user-notifications-clicked="openUserNotificationsTab"
|
||||
@favorite-menu-clicked="onSectionMenuClicked"
|
||||
@user-account-menu-clicked="openUserAccountTab"
|
||||
@place-menu-clicked="onGlobalPlaceChange" />
|
||||
</el-header>
|
||||
<!-- Содержимое -->
|
||||
<el-main class="supp-main-content">
|
||||
<div class="supp-content-wrapper">
|
||||
<el-tabs
|
||||
class="main-tabs"
|
||||
type="card"
|
||||
editable :addable="false"
|
||||
@edit="onEdit"
|
||||
v-model:model-value="activePaneKey">
|
||||
<template #add-icon>
|
||||
<div class="supp-tabs-menu">
|
||||
<el-dropdown>
|
||||
<el-button class="supp-action-button" size="small" :icon="ArrowDown" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="p in panes"
|
||||
:key="p.key"
|
||||
@click.native="activePaneKey = p.key">
|
||||
<span :style="{fontWeight: activePaneKey === p.key ? 'bold' : ''}">
|
||||
{{ p.key === '1' ? t('home_page') : t(p.title) }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-tooltip effect="dark" placement="top" :content="t('close_all')" :open-delay="100">
|
||||
<el-button class="supp-action-button" size="small" :icon="Close" @click="closeAllTabs" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tab-pane
|
||||
v-for="pane in panes"
|
||||
:key="pane.key"
|
||||
:closable="pane.closable"
|
||||
:name="pane.key">
|
||||
|
||||
<template #label>
|
||||
<div class="supp-pain-content-label" v-middle-click-close="pane.closable ? () => remove(pane.key) : null">
|
||||
<el-icon>
|
||||
<HomeFilled v-if="pane.key === '1'" />
|
||||
<Grid v-else />
|
||||
</el-icon>
|
||||
<span v-if="pane.key !== '1'">{{ pane.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Компоненты для отображения во вкладках -->
|
||||
<loading-view v-if="loading" />
|
||||
<main-tab-item
|
||||
v-show="!loading"
|
||||
:pane="pane"
|
||||
@help="onHelp(pane?.key)"
|
||||
@close="remove(pane?.key)"
|
||||
@closeEditForm="pane.additionalData = {}"
|
||||
@update:additionalData="(additionalData?: any) => pane.additionalData = additionalData"
|
||||
@update:entityId="(id?: number) => pane.entityId = id" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-main>
|
||||
<!-- Нижняя секция / футер -->
|
||||
<el-footer>
|
||||
<el-row style="height: 100%; align-items: center">
|
||||
<el-col :span="12">
|
||||
{{ t('supp_forms') }}. {{ t('version') }} {{ suppVersion }}
|
||||
</el-col>
|
||||
<el-col :span="12" style="text-align: right">
|
||||
{{ t('it_prom') }} 2023 - {{ dayjs().format('YYYY') }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-footer>
|
||||
|
||||
<el-dialog v-model="helpVisible" width="768px" :close-on-click-modal="false">
|
||||
<template #header>
|
||||
<div style="display: flex; align-items: center; font-size: 18px;">
|
||||
<el-icon style="color: var(--el-color-primary); font-size: 24px; margin-right: 10px;">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
<span style="font-weight: 500;">{{ t('help_title') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-html="helpText" style="word-break: break-word; line-height: 1.5;"></div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button v-if="userStore.canEdit('15')" type="primary" @click="openHelpTab">
|
||||
{{ t('edit') }}
|
||||
</el-button>
|
||||
<el-button @click="helpVisible = false">
|
||||
{{ t('close') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, onBeforeMount, onBeforeUnmount, provide, ref, watch } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import type { StompSubscription } from '@stomp/stompjs';
|
||||
import {
|
||||
AdminSection,
|
||||
messageError,
|
||||
t,
|
||||
useAdminStore,
|
||||
useUserStore,
|
||||
websocketService,
|
||||
UserSection, navigate
|
||||
} from '@itprom/core';
|
||||
import { LoadingView, Logo } from '@itprom/core-components';
|
||||
import { placeService } from '@itprom/place';
|
||||
import { suppSettingService } from '@itprom/settings';
|
||||
import MainTabItem from '@/components/MainTabItem.vue';
|
||||
import { ArrowDown, Close, Grid, HomeFilled, Moon, QuestionFilled, Sunny } from '@element-plus/icons-vue';
|
||||
import { router } from '@/router/router.ts';
|
||||
import { menuItemService, SuppSectionMenu, SuppUserMenu } from '@itprom/menu';
|
||||
import { helpService } from '@itprom/help';
|
||||
import { userProfileService } from '@itprom/user-profile';
|
||||
|
||||
/** Store */
|
||||
const userStore = useUserStore();
|
||||
const adminStore = useAdminStore();
|
||||
|
||||
/** Inject */
|
||||
const suppVersion = inject<string | number>('suppVersion', '1.0.0');
|
||||
|
||||
/** Data */
|
||||
const loading = ref(false);
|
||||
const globalPlaceChangeToggle = ref(false);
|
||||
const helpId = ref(-1);
|
||||
const helpText = ref('');
|
||||
const paneStack = ref(['1']);
|
||||
const helpVisible = ref(false);
|
||||
const activeDarkTheme = ref(userStore.theme === 'dark');
|
||||
|
||||
let placeSubscription: StompSubscription | undefined;
|
||||
let settingsSubscription: StompSubscription | undefined;
|
||||
let menuItemSubscription: StompSubscription | undefined;
|
||||
|
||||
/** Computed */
|
||||
const panes = computed(() => userStore.panes);
|
||||
const placeItems = computed(() => adminStore.placeOptions);
|
||||
|
||||
const activePaneKey = computed({
|
||||
get() {
|
||||
if (panes.value.length < 2) {
|
||||
return '1';
|
||||
}
|
||||
return userStore.activePaneKey;
|
||||
},
|
||||
set(key) {
|
||||
userStore.activePaneKey = key;
|
||||
}
|
||||
});
|
||||
|
||||
const switchStyle = computed(() => {
|
||||
return {
|
||||
'--el-switch-on-color': activeDarkTheme.value ? '#2c2c2c' : '#f2f2f2',
|
||||
'--el-switch-off-color': activeDarkTheme.value ? '#2c2c2c' : '#b2b2b2',
|
||||
'--el-switch-border-color': activeDarkTheme.value ? '#4c4d4f' : '#dcdfe6'
|
||||
};
|
||||
});
|
||||
|
||||
/** Functions */
|
||||
async function onThemeChanged(active: any) {
|
||||
activeDarkTheme.value = active;
|
||||
try {
|
||||
const value = active ? 'dark' : 'default';
|
||||
await userProfileService.updateUserTheme(userStore.userId, value);
|
||||
userStore.setTheme(value);
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
function add(key: string, title: string) {
|
||||
userStore.addPane({ title, content: '', key, closable: +key > 1 });
|
||||
}
|
||||
|
||||
function remove(key: string) {
|
||||
// Удаляем вкладку из стека
|
||||
removeFromPaneStack(key);
|
||||
|
||||
// Возвращаем из стека последнюю активную вкладку
|
||||
if (key === activePaneKey.value) {
|
||||
activePaneKey.value = paneStack.value.pop() as string;
|
||||
}
|
||||
|
||||
// По умолчанию переходим к начальной вкладке
|
||||
if (!activePaneKey.value) {
|
||||
activePaneKey.value = '1';
|
||||
}
|
||||
|
||||
return userStore.removePane(key);
|
||||
}
|
||||
|
||||
function removeFromPaneStack(val: string) {
|
||||
const index = paneStack.value.indexOf(val);
|
||||
if (index > -1) {
|
||||
paneStack.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function onEdit(targetKey: string) {
|
||||
if (targetKey) {
|
||||
remove(targetKey);
|
||||
}
|
||||
}
|
||||
|
||||
function closeAllTabs() {
|
||||
userStore.removeAllPanes();
|
||||
}
|
||||
|
||||
function onHelp(key: string) {
|
||||
// Подстановка номера страницы для начальной страницы
|
||||
if (key === '1') {
|
||||
key = userStore.defaultPage?.num ?? '';
|
||||
}
|
||||
|
||||
helpText.value = '';
|
||||
helpId.value = -1;
|
||||
|
||||
|
||||
helpService.getByPageNum(key)
|
||||
.then((response: any) => {
|
||||
helpText.value = response.data.text;
|
||||
helpId.value = response.data.id;
|
||||
})
|
||||
.catch(() => helpText.value = t('no_help_text'));
|
||||
|
||||
helpVisible.value = true;
|
||||
}
|
||||
|
||||
function openHelpTab() {
|
||||
helpVisible.value = false;
|
||||
|
||||
// Ищем, открыта ли уже вкладка помощи
|
||||
const found = panes.value.find(p => p.key === '15');
|
||||
|
||||
// Подстановка ключа текущего раздела для возврата/привязки
|
||||
let paneKey = activePaneKey.value;
|
||||
if (paneKey === '1') {
|
||||
paneKey = userStore.defaultPage?.num ?? '';
|
||||
}
|
||||
|
||||
if (found) {
|
||||
found.entityId = helpId.value;
|
||||
found.additionalData = { pageNum: paneKey };
|
||||
activePaneKey.value = '15';
|
||||
} else {
|
||||
userStore.addPane({
|
||||
title: t('supp-page-15'),
|
||||
content: '',
|
||||
key: '15',
|
||||
closable: true,
|
||||
entityId: helpId.value,
|
||||
additionalData: { pageNum: paneKey }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onSectionMenuClicked(num: string) {
|
||||
if (num) {
|
||||
add(num, t(`supp-page-${num}`));
|
||||
}
|
||||
}
|
||||
|
||||
function openUserAccountTab() {
|
||||
add(UserSection.USER_MENU.num, t(UserSection.USER_MENU.suppPageNum));
|
||||
}
|
||||
|
||||
function openUserNotificationsTab() {
|
||||
navigate('/user-settings/notifications');
|
||||
}
|
||||
|
||||
async function loadPlaceOptions() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await placeService.getPlaceOptions();
|
||||
adminStore.placeOptions = response ? response.data : [];
|
||||
} catch (error) {
|
||||
messageError(error, 'places_loading_error');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await suppSettingService.getAll();
|
||||
adminStore.settings = response ? response.data : [];
|
||||
} catch (error) {
|
||||
messageError(error, 'supp_setting_loading_error');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMenuItems() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await menuItemService.getTree();
|
||||
adminStore.menuItemOptions = response ? response.data : [];
|
||||
} catch (error) {
|
||||
messageError(error, 'menu_item_loading_error');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
userStore.logout();
|
||||
router.push('/login');
|
||||
}
|
||||
|
||||
function onGlobalPlaceChange(placeId: number | undefined) {
|
||||
userStore.setGlobalPlaceId(placeId);
|
||||
globalPlaceChangeToggle.value = !globalPlaceChangeToggle.value;
|
||||
}
|
||||
|
||||
/** Директива для обработки клика колесиком по всей вкладке */
|
||||
const vMiddleClickClose = {
|
||||
mounted: (el: HTMLElement, binding: any) => {
|
||||
if (!binding.value) return;
|
||||
|
||||
const tabItem = el.closest('.el-tabs__item') as HTMLElement;
|
||||
if (tabItem) {
|
||||
// Вешаем событие mousedown (нажатие)
|
||||
tabItem.onmousedown = (e: MouseEvent) => {
|
||||
if (e.button === 1) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
binding.value();
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
beforeUnmount: (el: HTMLElement) => {
|
||||
const tabItem = el.closest('.el-tabs__item') as HTMLElement;
|
||||
if (tabItem) {
|
||||
tabItem.onmousedown = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** Hooks */
|
||||
onBeforeMount(async () => {
|
||||
await loadPlaceOptions();
|
||||
await loadSettings();
|
||||
await loadMenuItems();
|
||||
|
||||
placeSubscription = websocketService.subscribe(AdminSection.PLACE.num, loadPlaceOptions);
|
||||
settingsSubscription = websocketService.subscribe(AdminSection.SETTINGS.num, loadSettings);
|
||||
menuItemSubscription = websocketService.subscribe(AdminSection.PROGRAM_MENU.num, loadMenuItems);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (placeSubscription) {
|
||||
websocketService.unsubscribe(placeSubscription);
|
||||
}
|
||||
|
||||
if (settingsSubscription) {
|
||||
websocketService.unsubscribe(settingsSubscription);
|
||||
}
|
||||
|
||||
if (menuItemSubscription) {
|
||||
websocketService.unsubscribe(menuItemSubscription);
|
||||
}
|
||||
});
|
||||
|
||||
/** Watch */
|
||||
watch(activePaneKey, (newKey) => {
|
||||
removeFromPaneStack(newKey);
|
||||
paneStack.value.push(newKey);
|
||||
});
|
||||
|
||||
/** Provide */
|
||||
provide('globalPlaceChangeToggle', globalPlaceChangeToggle);
|
||||
</script>
|
||||
|
||||
<!--suppress CssUnusedSymbol -->
|
||||
<style scoped>
|
||||
.user-menu {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.section-menu {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
.user-theme-switch :deep(.el-icon) > svg {
|
||||
color: #4a4a4a
|
||||
}
|
||||
</style>
|
||||
94
frontend/src/components/MainTabItem.vue
Normal file
94
frontend/src/components/MainTabItem.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<!-- Компонент временно лежит в корне приложения для удобства (в дальнейшем необходимо перенести в core-components) -->
|
||||
<template>
|
||||
<component
|
||||
v-if="currentComponent"
|
||||
:is="currentComponent"
|
||||
:entity-id="pane?.entityId"
|
||||
:additional-data="pane?.additionalData"
|
||||
@closeEditForm="$emit('@closeEditForm')"
|
||||
@update:componentInnerTabKey="onInnerKeyUpdate"
|
||||
@update:additionalData="(additionalData?: any) => $emit('@update:additionalData', additionalData)"
|
||||
@update:entityId="(id?: number) => $emit('@update:entityId', id)" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, inject, provide, type Ref, ref, watch } from 'vue';
|
||||
import { ModuleRegistry, useUserStore } from '@itprom/core';
|
||||
|
||||
/** Props */
|
||||
const props = defineProps(['pane']);
|
||||
|
||||
/** Store */
|
||||
const userStore = useUserStore();
|
||||
|
||||
/** Inject */
|
||||
const moduleRegistry: ModuleRegistry | undefined = inject('moduleRegistry');
|
||||
|
||||
/** Data */
|
||||
const currentTabSuppPageKey = ref(props.pane?.key);
|
||||
const childComponentInnerTabKey = ref<string | undefined>();
|
||||
|
||||
/*
|
||||
Словарь ключей для проверки доступов к компонентам. Если ключ компонента для его монтирования
|
||||
(который объявляется в массивах *-tab-items) не совпадает с номером секции (supp-page-num),
|
||||
то необходимо для этого ключа указать список номеров соответствующих разделов приложения.
|
||||
|
||||
Например, компонент под номером '17' содержит сразу 2 страницы приложения - '17-language' и '17-messageL10n'.
|
||||
Для каждой из них права доступа через роли устанавливаются отдельно, поэтому они должны быть определены в списке ниже.
|
||||
*/
|
||||
const specialSectionsMap: Map<string, any> = new Map([
|
||||
]);
|
||||
|
||||
/** Computed */
|
||||
const currentTabKey = computed(() => {
|
||||
// Специальная обработка для домашней страницы
|
||||
if (props.pane?.key === '1') {
|
||||
return userStore.defaultPage?.num;
|
||||
}
|
||||
|
||||
return props.pane?.key;
|
||||
});
|
||||
|
||||
const currentComponent = computed(() => {
|
||||
const moduleInfo = moduleRegistry?.getModule(currentTabKey.value);
|
||||
return moduleInfo?.component
|
||||
? defineAsyncComponent(moduleInfo.component)
|
||||
: undefined;
|
||||
});
|
||||
|
||||
const canEdit: Ref<boolean> = ref(userStore.canEdit(currentTabKey.value));
|
||||
|
||||
/** Functions */
|
||||
function onInnerKeyUpdate(val: string) {
|
||||
childComponentInnerTabKey.value = val;
|
||||
}
|
||||
|
||||
/**
|
||||
Если ключ компонента для его монтирования (который объявляется в массивах *-tab-items)
|
||||
не совпадает с номером секции (supp-page-num), то при открытии такого компонента необходимо получить из него
|
||||
соответствующий номер секции и с помощью specialSectionsMap обновить состояние canEdit.
|
||||
|
||||
@see LocalizationComponent.vue
|
||||
*/
|
||||
function reEvaluateCanEditCondition() {
|
||||
const paneKey = props.pane?.key;
|
||||
const val = childComponentInnerTabKey.value;
|
||||
|
||||
// Специальная обработка для домашней страницы
|
||||
if (props.pane?.key === '1') {
|
||||
currentTabSuppPageKey.value = val;
|
||||
} else if (specialSectionsMap.get(paneKey)?.includes(val)) {
|
||||
currentTabSuppPageKey.value = val;
|
||||
} else {
|
||||
currentTabSuppPageKey.value = paneKey;
|
||||
}
|
||||
|
||||
canEdit.value = userStore.canEdit(currentTabSuppPageKey.value);
|
||||
}
|
||||
|
||||
/** Watch */
|
||||
watch(() => childComponentInnerTabKey.value, reEvaluateCanEditCondition);
|
||||
|
||||
/** Provide */
|
||||
provide('canEdit', canEdit);
|
||||
</script>
|
||||
61
frontend/src/main.ts
Normal file
61
frontend/src/main.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import ElementPlus from 'element-plus';
|
||||
import { createPinia } from 'pinia';
|
||||
import { messageL10nService } from '@itprom/message';
|
||||
import { router } from '@/router/router.ts';
|
||||
import { TABLE_LAYOUT_SERVICE_TOKEN, ModuleRegistry, DependencyRegistry, updateMessages } from '@itprom/core';
|
||||
import { layoutCoreAdapter } from '@itprom/tablelayout';
|
||||
|
||||
// Импорт глобальных стилей element plus
|
||||
import 'element-plus/dist/index.css';
|
||||
import './styles/itprom-modules.css';
|
||||
import './styles/index.scss';
|
||||
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
||||
import { initModuleRegistry } from '@/modules.ts';
|
||||
import { favoriteService } from '@itprom/favorites';
|
||||
|
||||
/**
|
||||
* Основная точка входа Vue-приложения.
|
||||
* Инициализация приложения, глобальных плагинов и настроек.
|
||||
*
|
||||
* @author Sergey Verbitsky
|
||||
* @since 09.12.2025
|
||||
*/
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
// Локальное хранилище
|
||||
app.use(createPinia());
|
||||
|
||||
// Роутер
|
||||
app.use(router);
|
||||
|
||||
// Сервис локализации для корректной работы i18n (см. core/src/i18n/translation.ts)
|
||||
app.provide('l10Service', messageL10nService);
|
||||
app.provide('favoriteService', favoriteService);
|
||||
|
||||
// Регистрация сервисов внешних модулей, нужных в core и core-components
|
||||
DependencyRegistry.register(TABLE_LAYOUT_SERVICE_TOKEN, layoutCoreAdapter);
|
||||
|
||||
// Версия СУПП
|
||||
app.provide('suppVersion', '1.0.0');
|
||||
|
||||
// Подключение компонентов Element plus
|
||||
app.use(ElementPlus);
|
||||
|
||||
// Асинхронная инициализация и запуск
|
||||
async function startApp() {
|
||||
// Подключаем СУПП модули
|
||||
const registry: ModuleRegistry = await initModuleRegistry();
|
||||
app.provide('moduleRegistry', registry);
|
||||
|
||||
// Загружаем локализацию до монтирования
|
||||
await updateMessages(messageL10nService);
|
||||
|
||||
// Монтирование главного компонента приложения
|
||||
app.mount('#app');
|
||||
}
|
||||
|
||||
startApp()
|
||||
.catch((err: any) => console.log(err));
|
||||
42
frontend/src/modules.ts
Normal file
42
frontend/src/modules.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ModuleRegistry } from '@itprom/core';
|
||||
|
||||
/**
|
||||
* Список подключаемых/импортируемых модулей СУПП
|
||||
*/
|
||||
const modules: Record<string, () => Promise<any>> = {
|
||||
coreComponents: () => import('@itprom/core-components'),
|
||||
menu: () => import('@itprom/menu'),
|
||||
favorites: () => import('@itprom/favorites'),
|
||||
place: () => import('@itprom/place'),
|
||||
settings: () => import('@itprom/settings'),
|
||||
language: () => import('@itprom/language'),
|
||||
user: () => import('@itprom/user'),
|
||||
userProfile: () => import('@itprom/user-profile'),
|
||||
department: () => import('@itprom/department'),
|
||||
person: () => import('@itprom/person'),
|
||||
role: () => import('@itprom/role'),
|
||||
help: () => import('@itprom/help'),
|
||||
message: () => import('@itprom/message'),
|
||||
// shift: () => import('@itprom/shift'),
|
||||
// shiftSeq: () => import('@itprom/shift-seq'),
|
||||
// profession: () => import('@itprom/profession'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Реестр конфигураций подключаемых модулей СУПП
|
||||
*/
|
||||
export async function initModuleRegistry(): Promise<ModuleRegistry> {
|
||||
const registry = new ModuleRegistry();
|
||||
|
||||
for (const loadModule of Object.values(modules)) {
|
||||
const mod = await loadModule();
|
||||
const info = mod.moduleInfo;
|
||||
|
||||
// если нет номера — это общий/утилитарный модуль
|
||||
if (!info?.num) continue;
|
||||
|
||||
registry.registerModule(info);
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
2
frontend/src/modules/readme.md
Normal file
2
frontend/src/modules/readme.md
Normal file
@@ -0,0 +1,2 @@
|
||||
В папке modules находятся модули, специфичные для конкретного проекта.
|
||||
Прочие универсальные модули должны подключаться как npm зависимости.
|
||||
120
frontend/src/router/router.ts
Normal file
120
frontend/src/router/router.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import type { AxiosError, AxiosResponse } from 'axios';
|
||||
import { Login, Register, PageNotFound } from '@itprom/core-components';
|
||||
import { useUserStore, AXIOS, isNotBlank, authenticationService } from '@itprom/core';
|
||||
import Main from '@/components/Main.vue';
|
||||
|
||||
/**
|
||||
* Базовый маршрутизатор программы для навигации по страницам приложения.
|
||||
*
|
||||
* @author Sergey Verbitsky
|
||||
* @since 09.12.2025
|
||||
*/
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'main',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: Register
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'pageNotFound',
|
||||
component: PageNotFound
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Конфигурация проверки аутентификации пользователей перед каждым переходом между страницами
|
||||
router.beforeEach(async (to, _from) => {
|
||||
const userStore = useUserStore();
|
||||
|
||||
// Публичные страницы не нуждаются в проверке
|
||||
if (['login', 'register', 'pageNotFound'].includes(to.name as string)) return;
|
||||
|
||||
// Если токена нет вовсе - немедленно перенаправляем пользователя на страницу входа
|
||||
if (!userStore.token) {
|
||||
return { name: 'login' };
|
||||
}
|
||||
|
||||
// Если токен есть, но статус аутентификации в течение текущей сессии еще не был проверен - отправляем запрос на проверку
|
||||
if (!userStore.isAuthChecked) {
|
||||
try {
|
||||
const res: AxiosResponse<boolean> = await authenticationService.checkAuthentication();
|
||||
if (res.data) {
|
||||
// Если пользователь аутентифицирован - выставляем флаг в userStore, а также заголовок 'Bearer'
|
||||
// для всех последующих запросов. И разрешаем переход на нужную страницу
|
||||
userStore.isAuthChecked = true;
|
||||
setupBearerTokenHeader();
|
||||
return;
|
||||
} else {
|
||||
// Иначе (и при исключении) - очищаем статус аутентификации и перенаправляем на страницу входа
|
||||
userStore.logout();
|
||||
return { name: 'login' };
|
||||
}
|
||||
} catch {
|
||||
userStore.logout();
|
||||
return { name: 'login' };
|
||||
}
|
||||
}
|
||||
|
||||
// Если статус аутентификации уже проверен - выставляем заголовок 'Bearer' для всех последующих запросов.
|
||||
setupBearerTokenHeader();
|
||||
});
|
||||
|
||||
/**
|
||||
* Централизованное место управления конфигурацией AXIOS.
|
||||
* Позволяет устанавливать/сбрасывать необходимые дефолтные заголовки или обработчики событий
|
||||
*
|
||||
* @author Ilia Malafeev
|
||||
* @since 15.08.2025
|
||||
*/
|
||||
|
||||
// Флаг хранит отметку о том, что перехватчики уже установлены, чтобы не устанавливать их дважды
|
||||
let interceptorsInstalled = false;
|
||||
|
||||
// Перехват ошибок запросов к серверу
|
||||
export function setupAxiosInterceptors() {
|
||||
if (interceptorsInstalled) return;
|
||||
interceptorsInstalled = true;
|
||||
|
||||
AXIOS.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
// При 401 статусе нужно сбросить состояния аутентификации пользователя и перенаправить его на страницу входа
|
||||
if (error?.response?.status === 401) {
|
||||
const userStore = useUserStore();
|
||||
userStore.logout();
|
||||
|
||||
// Тк происходит logout, нужно сбросить заголовок с jwt перед следующей аутентификацией
|
||||
delete AXIOS.defaults.headers.common.Authorization;
|
||||
|
||||
if (router.currentRoute.value?.name !== 'login') {
|
||||
router.push({ name: 'login' });
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function setupBearerTokenHeader() {
|
||||
const userStore = useUserStore();
|
||||
if (isNotBlank(userStore.token)) {
|
||||
AXIOS.defaults.headers.common['Authorization'] = `Bearer ${userStore.token}`;
|
||||
} else {
|
||||
delete AXIOS.defaults.headers.common.Authorization;
|
||||
}
|
||||
}
|
||||
59
frontend/src/scripts/build-itprom-css.js
Normal file
59
frontend/src/scripts/build-itprom-css.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/**
|
||||
* Скрипт собирает все scoped стили модулей в 1 файл для подключения всех стилей в main.ts
|
||||
*
|
||||
* @author Sergey Verbitsky
|
||||
* @since 12.01.2026
|
||||
*/
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
const workspaceModules = path.join(root, 'src/modules');
|
||||
const nodeModules = path.join(root, 'node_modules/@itprom');
|
||||
const outFile = path.join(root, 'src/styles/itprom-modules.css');
|
||||
|
||||
let cssContent = '';
|
||||
|
||||
function collectCss(distDir) {
|
||||
if (!fs.existsSync(distDir)) return;
|
||||
|
||||
const files = fs.readdirSync(distDir);
|
||||
|
||||
files
|
||||
.filter(f => f.endsWith('.css'))
|
||||
.forEach(css => {
|
||||
const fullPath = path.join(distDir, css);
|
||||
const content = fs.readFileSync(fullPath, 'utf8');
|
||||
|
||||
cssContent += `/* ${path.basename(distDir)} */\n`;
|
||||
cssContent += content + '\n\n';
|
||||
});
|
||||
}
|
||||
|
||||
if (fs.existsSync(workspaceModules)) {
|
||||
const modules = fs.readdirSync(workspaceModules);
|
||||
|
||||
modules.forEach(m => {
|
||||
const dist = path.join(workspaceModules, m, 'dist');
|
||||
collectCss(dist);
|
||||
});
|
||||
}
|
||||
|
||||
if (fs.existsSync(nodeModules)) {
|
||||
const modules = fs.readdirSync(nodeModules);
|
||||
|
||||
modules.forEach(m => {
|
||||
const dist = path.join(nodeModules, m, 'dist');
|
||||
collectCss(dist);
|
||||
});
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
||||
fs.writeFileSync(outFile, cssContent, 'utf8');
|
||||
|
||||
console.log(`✔ modules CSS bundle generated: ${outFile}`);
|
||||
3
frontend/src/shims-vue.d.ts
vendored
Normal file
3
frontend/src/shims-vue.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module '*.vue';
|
||||
declare module '*.ts';
|
||||
declare module '*.js';
|
||||
111
frontend/src/styles/_components.scss
Normal file
111
frontend/src/styles/_components.scss
Normal file
@@ -0,0 +1,111 @@
|
||||
.supp-action-button,
|
||||
.supp-tabs-menu-button {
|
||||
height: 32px !important;
|
||||
width: 32px !important;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
vertical-align: middle;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
// Стили для кнопок у которых не задан type primary, danger и т.д.
|
||||
&:not(.el-button--primary):not(.el-button--danger):not(.el-button--success):not(.el-button--warning) {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--el-border-color);
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
&:active {
|
||||
border-color: var(--el-color-primary-dark-2);
|
||||
color: var(--el-color-primary-dark-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.supp-header-icon {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-regular);
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.bordered-table {
|
||||
border: 1px solid #e8e8e8;
|
||||
border-collapse: collapse;
|
||||
padding: 0.3rem;
|
||||
|
||||
td {
|
||||
border: 1px solid #e8e8e8;
|
||||
border-collapse: collapse;
|
||||
padding: 0.3rem;
|
||||
|
||||
&.td-padding {
|
||||
padding: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
border: 1px solid #e8e8e8;
|
||||
border-collapse: collapse;
|
||||
padding: 0.2rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: var(--el-text-color-primary) !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Адаптация el-form-item для компактного отображения внутри ячеек таблицы
|
||||
========================================================================== */
|
||||
.el-form-item.mb-disable {
|
||||
margin-bottom: 0;
|
||||
|
||||
.el-form-item__content {
|
||||
display: block;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.el-form-item__error {
|
||||
position: static;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
margin-top: 0;
|
||||
transition: max-height 0.3s ease, opacity 0.3s ease, margin-top 0.3s ease;
|
||||
}
|
||||
|
||||
&.is-error .el-form-item__error {
|
||||
max-height: 150px;
|
||||
opacity: 1;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Стиль для кнопок, используемых в таблицах детальных записей (т.к. size="small" выдает прямоугольную кнопку ) */
|
||||
.supp-small-button {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
147
frontend/src/styles/_layout.scss
Normal file
147
frontend/src/styles/_layout.scss
Normal file
@@ -0,0 +1,147 @@
|
||||
:root {
|
||||
--bg-header: #001529;
|
||||
--bg-content: #FFFFFF;
|
||||
--bg-footer: #f5f5f5;
|
||||
|
||||
/* Цвета личного кабинета */
|
||||
--supp-user-profile-drag-border: #ccc;
|
||||
--supp-user-profile-drag-bg: #f0f0f0;
|
||||
--supp-user-profile-danger: #c12127;
|
||||
--supp-user-profile-danger-hover: #bd565a;
|
||||
--supp-user-profile-primary: #1668dc;
|
||||
--supp-user-profile-primary-hover: #5c90d9;
|
||||
--supp-user-profile-success: #3aaf0c;
|
||||
--supp-user-profile-success-hover: #6bb74d;
|
||||
}
|
||||
|
||||
[data-color-scheme="dark"] {
|
||||
--bg-header: #1d1e1f;
|
||||
--bg-content: #141414;
|
||||
--bg-footer: #1d1e1f;
|
||||
}
|
||||
|
||||
.supp-app-container {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
.el-main {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.el-header {
|
||||
background-color: var(--bg-header);
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
--el-menu-bg-color: var(--bg-header);
|
||||
--el-menu-text-color: rgba(255, 255, 255, 0.88);
|
||||
--el-menu-hover-text-color: #ffffff;
|
||||
--el-menu-active-color: rgba(255, 255, 255, 0.88);
|
||||
--el-menu-hover-bg-color: transparent;
|
||||
--el-menu-item-font-size: 12px;
|
||||
--el-menu-border-color: transparent;
|
||||
}
|
||||
|
||||
.el-footer {
|
||||
background-color: var(--bg-footer);
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.supp-main-content {
|
||||
height: 100%;
|
||||
padding: 0 !important;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.supp-content-wrapper {
|
||||
background: var(--bg-content);
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
transition: background 0.3s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
/* El-tabs должен занимать все доступное пространство */
|
||||
> .el-tabs {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-tabs__content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.el-tab-pane {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.supp-tabs-menu {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 4px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.supp-actions-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.supp-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.supp-form-item-actions {
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
|
||||
.el-form-item__content {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.supp-pain-content-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Класс для строки, которая должна растянуться на всю высоту (например, с таблицей) */
|
||||
.fill-height-row {
|
||||
flex: 1 !important;
|
||||
min-height: 0 !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* Колонка внутри такой строки тоже должна растянуться */
|
||||
.fill-height-row > .el-col {
|
||||
flex: 1 !important;
|
||||
height: 100% !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
316
frontend/src/styles/_overrides.scss
Normal file
316
frontend/src/styles/_overrides.scss
Normal file
@@ -0,0 +1,316 @@
|
||||
.el-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.el-tabs__new-tab {
|
||||
cursor: unset !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.main-tabs > .el-tabs__header {
|
||||
height: 32px !important;
|
||||
}
|
||||
|
||||
.main-tabs > .el-tabs__header .el-tabs__nav-scroll {
|
||||
height: 32px !important;
|
||||
}
|
||||
|
||||
.main-tabs > .el-tabs__header .el-tabs__item {
|
||||
height: 32px !important;
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
color: var(--el-text-color-regular);
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-bottom: 1px solid var(--el-border-color) !important;
|
||||
font-size: 12px !important;
|
||||
padding: 0 12px !important;
|
||||
|
||||
&#tab-1 .el-icon {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.el-icon:not(.is-icon-close) {
|
||||
font-size: 14px !important;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.is-icon-close {
|
||||
font-size: 14px !important;
|
||||
width: 14px !important;
|
||||
margin-left: 4px !important;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
color: var(--el-text-color-primary) !important;
|
||||
font-weight: 900 !important;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: var(--bg-content) !important;
|
||||
color: var(--el-color-primary) !important;
|
||||
|
||||
border-bottom: 1px solid var(--bg-content) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-card,
|
||||
.el-card.is-always-shadow,
|
||||
.el-card.is-hover-shadow:hover {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Темная тема для библиотек */
|
||||
[data-color-scheme="dark"] {
|
||||
.el-select .el-input__inner,
|
||||
.el-input.is-disabled .el-input__inner,
|
||||
.apexcharts-legend-text {
|
||||
color: rgba(255, 255, 255, 0.85) !important;
|
||||
}
|
||||
|
||||
.apexcharts-menu {
|
||||
background: rgba(0, 0, 0) !important;
|
||||
}
|
||||
|
||||
.apexcharts-theme-light .apexcharts-menu-item:hover {
|
||||
background: #353535;
|
||||
}
|
||||
|
||||
.el-table__summary {
|
||||
background: #1d1d1d !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Светлая тема для библиотек */
|
||||
[data-color-scheme="light"] {
|
||||
.el-select .el-input__inner,
|
||||
.el-input.is-disabled .el-input__inner,
|
||||
.apexcharts-legend-text {
|
||||
color: rgba(0, 0, 0, 0.85) !important;
|
||||
}
|
||||
|
||||
.apexcharts-menu {
|
||||
background: rgba(255, 255, 255) !important;
|
||||
}
|
||||
|
||||
.apexcharts-theme-light .apexcharts-menu-item:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.el-table__summary {
|
||||
background: #fafafa !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-popper:has(.el-menu--popup) {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.el-menu--popup {
|
||||
background-color: var(--bg-header) !important;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
|
||||
--el-menu-bg-color: var(--bg-header);
|
||||
--el-menu-text-color: rgba(255, 255, 255, 0.88);
|
||||
--el-menu-hover-text-color: #ffffff;
|
||||
--el-menu-active-color: rgba(255, 255, 255, 0.88);
|
||||
--el-menu-item-font-size: 12px;
|
||||
--el-menu-hover-bg-color: rgba(255, 255, 255, 0.1);
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
&:hover {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
color: rgba(255, 255, 255, 0.88) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Заголовок карточки таблицы и формы редактирования */
|
||||
.table-card-content-wrapper,
|
||||
.edit-card-content-wrapper {
|
||||
.el-card__header {
|
||||
padding: 10px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Заголовок карточки личного кабинета (уведомления) */
|
||||
.user-profile-card {
|
||||
.el-card__header {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-message-card {
|
||||
.el-card__header {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.favorite-item-card {
|
||||
.el-card__body {
|
||||
padding: 0 1rem;
|
||||
align-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Стили для компоненты выбора дат/времени */
|
||||
.date-picker-container .el-date-editor {
|
||||
width: 260px !important;
|
||||
height: 32px !important;
|
||||
padding: 0 0 0 8px;
|
||||
border-radius: 6px;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.date-picker-container .el-range-separator {
|
||||
flex: none;
|
||||
width: auto;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.date-picker-container .el-range-input {
|
||||
flex: none;
|
||||
width: 110px !important;
|
||||
text-align: center !important;
|
||||
color: var(--el-text-color-primary) !important;
|
||||
height: 28px !important;
|
||||
}
|
||||
|
||||
.date-picker-container .el-range__close-icon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Базовые стили для тела карточки */
|
||||
.table-card-content-wrapper .el-card__body,
|
||||
.edit-card-content-wrapper .el-card__body {
|
||||
flex: 1 !important;
|
||||
min-height: 0 !important;
|
||||
width: 100%;
|
||||
padding: 12px !important;
|
||||
box-sizing: border-box !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
/* Обычные строки внутри карточек (поиск, фильтры) не должны сжиматься */
|
||||
.table-card-content-wrapper .el-card__body > .el-row:not(.fill-height-row) {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Стили для поповера фильтрации в таблице */
|
||||
.el-popover.table-filter-popover {
|
||||
padding: 0 !important;
|
||||
min-width: unset !important;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.table-filter-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
/* Основной контейнер */
|
||||
.filter-content {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Список радио-кнопок */
|
||||
.filter-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.el-radio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
margin-right: 0;
|
||||
padding: 0 8px;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
box-sizing: border-box;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.el-radio__label {
|
||||
font-weight: normal;
|
||||
padding-left: 8px;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-radio__input {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Разделитель */
|
||||
.filter-divider {
|
||||
height: 1px;
|
||||
background-color: var(--el-border-color-lighter);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Футер с кнопками */
|
||||
.filter-footer {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
border-radius: 0 0 var(--el-border-radius-base) var(--el-border-radius-base);
|
||||
.btn-reset {
|
||||
color: var(--el-text-color-secondary) !important;
|
||||
font-weight: normal;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-primary) !important;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--el-text-color-disabled) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Теги колонки TagsTableColumn */
|
||||
.supp-ellipsis-tag .el-tag__content {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.el-table th.el-table__cell {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
55
frontend/src/styles/_reset.scss
Normal file
55
frontend/src/styles/_reset.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
:root {
|
||||
--el-border-radius-base: 6px !important;
|
||||
--el-border-radius-small: 4px !important;
|
||||
--el-border-radius-round: 20px !important;
|
||||
|
||||
--el-text-color-primary: rgba(0, 0, 0, 0.88);
|
||||
--el-text-color-regular: rgba(0, 0, 0, 0.88);
|
||||
--el-text-color-secondary: rgba(0, 0, 0, 0.65);
|
||||
--el-text-color-placeholder: rgba(0, 0, 0, 0.25);
|
||||
--el-border-color: #d9d9d9;
|
||||
--el-border-color-light: #e5e5e5;
|
||||
--el-border-color-lighter: #f0f0f0;
|
||||
--el-border-color-extra-light: #f5f5f5;
|
||||
|
||||
--el-color-primary: #1677ff;
|
||||
--el-color-primary-light-3: #4096ff;
|
||||
--el-color-primary-light-5: #69b1ff;
|
||||
--el-color-primary-light-7: #91caff;
|
||||
--el-color-primary-light-8: #bae0ff;
|
||||
--el-color-primary-light-9: #e6f4ff;
|
||||
--el-color-primary-dark-2: #0958d9;
|
||||
|
||||
--el-fill-color-blank: #ffffff;
|
||||
--el-fill-color: #f5f5f5;
|
||||
--el-fill-color-light: #fafafa;
|
||||
--el-fill-color-lighter: #ffffff;
|
||||
--el-fill-color-extra-light: #ffffff;
|
||||
|
||||
--el-box-shadow-light: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02);
|
||||
--el-box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
[data-color-scheme="dark"] {
|
||||
--el-text-color-primary: rgba(255, 255, 255, 0.90);
|
||||
--el-text-color-regular: rgba(255, 255, 255, 0.75);
|
||||
--el-text-color-secondary: rgba(255, 255, 255, 0.45);
|
||||
--el-text-color-placeholder: rgba(255, 255, 255, 0.25);
|
||||
|
||||
--el-border-color: rgba(255, 255, 255, 0.15);
|
||||
--el-border-color-light: rgba(255, 255, 255, 0.1);
|
||||
--el-border-color-lighter: rgba(255, 255, 255, 0.08);
|
||||
|
||||
--el-fill-color: rgba(255, 255, 255, 0.04);
|
||||
--el-fill-color-light: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
body, #app {
|
||||
margin: 0;
|
||||
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
5
frontend/src/styles/index.scss
Normal file
5
frontend/src/styles/index.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
// Точка входа стилей
|
||||
@use './reset';
|
||||
@use './layout';
|
||||
@use './components';
|
||||
@use './overrides';
|
||||
24
frontend/src/styles/itprom-modules.css
Normal file
24
frontend/src/styles/itprom-modules.css
Normal file
File diff suppressed because one or more lines are too long
21
frontend/tsconfig.app.json
Normal file
21
frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": false,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
9
frontend/tsconfig.base.json
Normal file
9
frontend/tsconfig.base.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@itprom/*": ["./src/modules/*/src/index.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
29
frontend/tsconfig.json
Normal file
29
frontend/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@itprom/*": ["./src/modules/*/src/index.ts"]
|
||||
},
|
||||
"types": ["vite/client"],
|
||||
"strict": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "vue",
|
||||
"moduleResolution": "bundler",
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*", "src/modules/**/dist"]
|
||||
}
|
||||
7
frontend/tsconfig.node.json
Normal file
7
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.json" }
|
||||
]
|
||||
}
|
||||
16
frontend/turbo.json
Normal file
16
frontend/turbo.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^lint"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
77
frontend/vite.config.ts
Normal file
77
frontend/vite.config.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import checker from 'vite-plugin-checker';
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
checker({
|
||||
vueTsc: { tsconfigPath: './tsconfig.app.json' },
|
||||
typescript: true
|
||||
}),
|
||||
Components({
|
||||
dirs: ['src'],
|
||||
include: [/\.vue$/, /\.vue\?vue/, /\.vue\.[tj]sx?\?vue/, /\.[tj]sx?$/],
|
||||
dts: true,
|
||||
resolvers: [ElementPlusResolver()]
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolvePath('./src'),
|
||||
...getModuleAliases()
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'target/dist',
|
||||
assetsDir: 'static'
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:9001',
|
||||
ws: true,
|
||||
changeOrigin: true
|
||||
},
|
||||
'/websocket': {
|
||||
target: 'ws://localhost:9001',
|
||||
ws: true,
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
define: {
|
||||
global: 'globalThis'
|
||||
}
|
||||
}));
|
||||
|
||||
const resolvePath = (p: string) => fileURLToPath(new URL(p, import.meta.url));
|
||||
|
||||
/**
|
||||
* Автоматически генерирует алиасы для модулей.
|
||||
* Ищет папки в src/modules и создает маппинг:
|
||||
* @itprom/<folder> -> src/modules/<folder>/src/index.ts
|
||||
*/
|
||||
function getModuleAliases() {
|
||||
const modulesDir = resolvePath('./src/modules');
|
||||
const aliases: Record<string, string> = {};
|
||||
|
||||
if (fs.existsSync(modulesDir)) {
|
||||
const folders = fs.readdirSync(modulesDir);
|
||||
folders.forEach((folder) => {
|
||||
// Проверяем, что это папка и внутри есть src/index.ts
|
||||
const moduleSrcPath = path.join(modulesDir, folder, 'src', 'index.ts');
|
||||
if (fs.existsSync(moduleSrcPath)) {
|
||||
aliases[`@itprom/${folder}`] = moduleSrcPath;
|
||||
}
|
||||
});
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
295
mvnw
vendored
Normal file
295
mvnw
vendored
Normal file
@@ -0,0 +1,295 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
189
mvnw.cmd
vendored
Normal file
189
mvnw.cmd
vendored
Normal file
@@ -0,0 +1,189 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
|
||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
$MAVEN_WRAPPER_DISTS = $null
|
||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||
} else {
|
||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||
}
|
||||
|
||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
$actualDistributionDir = ""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||
$actualDistributionDir = $distributionUrlNameMain
|
||||
}
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if (!$actualDistributionDir) {
|
||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||
$actualDistributionDir = $_.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$actualDistributionDir) {
|
||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||
}
|
||||
|
||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
39
pom.xml
Normal file
39
pom.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<!-- Common project info -->
|
||||
<groupId>org.itprom</groupId>
|
||||
<artifactId>supp-forms-based-app</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<description>supp-forms-based-app</description>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<name>Sergey Verbitsky</name>
|
||||
<organization>It-prom</organization>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<!-- Java -->
|
||||
<properties>
|
||||
<java.version>25</java.version>
|
||||
<maven.deploy.skip>true</maven.deploy.skip>
|
||||
</properties>
|
||||
|
||||
<!-- Modules -->
|
||||
<modules>
|
||||
<module>frontend</module>
|
||||
<module>backend</module>
|
||||
</modules>
|
||||
|
||||
<!-- Modules publishing -->
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>nexus</id>
|
||||
<url>${env.NEXUS_REGISTRY_MAVEN}/</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
</project>
|
||||
Reference in New Issue
Block a user