Init base project files
This commit is contained in:
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;
|
||||
}
|
||||
Reference in New Issue
Block a user