Init base project files

This commit is contained in:
Sergei_Verbitski
2026-03-19 13:11:12 +03:00
parent 2cc375134a
commit 8575fd6db8
46 changed files with 11505 additions and 3 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf

64
.gitignore vendored Normal file
View 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
View 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
View 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"]

View File

@@ -1,3 +0,0 @@
# supp-forms-based-app
Базовый репозиторий для форка новых проектов, базирующихся на supp-forms

150
backend/app/pom.xml Normal file
View 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>

View File

@@ -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);
}
}

View 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
View 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

95
frontend/eslint.config.js Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

76
frontend/package.json Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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} создан`);

View 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."

View 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."

View 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."

View 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."

View 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
View 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>

View 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>

View 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
View 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
View 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;
}

View File

@@ -0,0 +1,2 @@
В папке modules находятся модули, специфичные для конкретного проекта.
Прочие универсальные модули должны подключаться как npm зависимости.

View 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;
}
}

View 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
View File

@@ -0,0 +1,3 @@
declare module '*.vue';
declare module '*.ts';
declare module '*.js';

View 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;
}

View 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;
}

View 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);
}

View 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;
}

View File

@@ -0,0 +1,5 @@
// Точка входа стилей
@use './reset';
@use './layout';
@use './components';
@use './overrides';

File diff suppressed because one or more lines are too long

View 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"]
}

View 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
View 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"]
}

View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.json" }
]
}

16
frontend/turbo.json Normal file
View 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
View 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
View 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
View 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
View 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>