init electron
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
# Путь к папке с шаблонами registry (относительно drafter)
|
||||
TEMPLATES_REGISTRY_PATH=../registry
|
||||
# Путь к папке cooptypes registry для синхронизации
|
||||
COOPTYPES_REGISTRY_PATH=../cooptypes/src/cooperative/registry
|
||||
@@ -1,4 +0,0 @@
|
||||
# Путь к папке с шаблонами registry (относительно drafter)
|
||||
TEMPLATES_REGISTRY_PATH=../registry
|
||||
# Путь к папке cooptypes registry для синхронизации
|
||||
COOPTYPES_REGISTRY_PATH=../cooptypes/src/cooperative/registry
|
||||
+1
-1
@@ -83,6 +83,6 @@ module.exports = {
|
||||
// '@typescript-eslint/no-explicit-any': 'off',
|
||||
// allow debugger during development only
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
name: Build and Release Electron App
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint code
|
||||
run: pnpm lint
|
||||
|
||||
build:
|
||||
needs: lint
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get package info
|
||||
id: package
|
||||
run: |
|
||||
echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
echo "NAME=$(node -p "require('./package.json').name")" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build Electron App for Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: pnpm build:electron
|
||||
env:
|
||||
PLATFORM: win32
|
||||
|
||||
- name: Build Electron App for macOS
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: pnpm build:electron
|
||||
env:
|
||||
PLATFORM: darwin
|
||||
|
||||
- name: Build Electron App for Linux
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: pnpm build:electron
|
||||
env:
|
||||
PLATFORM: linux
|
||||
|
||||
- name: Prepare for artifact upload
|
||||
id: prepare_artifacts
|
||||
run: |
|
||||
APP_NAME=${{ steps.package.outputs.NAME }}
|
||||
APP_VERSION=${{ steps.package.outputs.VERSION }}
|
||||
if [ "${{ matrix.os }}" == "windows-latest" ]; then
|
||||
echo "ARTIFACT_PATHS=dist/electron/*.exe" >> $GITHUB_OUTPUT
|
||||
echo "ARTIFACT_NAME=$APP_NAME-windows-$APP_VERSION" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ matrix.os }}" == "macos-latest" ]; then
|
||||
echo "ARTIFACT_PATHS=dist/electron/*.dmg" >> $GITHUB_OUTPUT
|
||||
echo "ARTIFACT_NAME=$APP_NAME-macos-$APP_VERSION" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "ARTIFACT_PATHS=dist/electron/*.AppImage" >> $GITHUB_OUTPUT
|
||||
echo "ARTIFACT_NAME=$APP_NAME-linux-$APP_VERSION" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.prepare_artifacts.outputs.ARTIFACT_NAME }}
|
||||
path: ${{ steps.prepare_artifacts.outputs.ARTIFACT_PATHS }}
|
||||
if-no-files-found: warn
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get package info
|
||||
id: package
|
||||
run: |
|
||||
echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
echo "NAME=$(node -p "require('./package.json').name")" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: ${{ steps.package.outputs.NAME }} v${{ steps.package.outputs.VERSION }}
|
||||
files: artifacts/**/*
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"types": [
|
||||
{ "type": "feat", "section": "Новые функции" },
|
||||
{ "type": "fix", "section": "Исправления ошибок" },
|
||||
{ "type": "chore", "hidden": true },
|
||||
{ "type": "docs", "section": "Документация" },
|
||||
{ "type": "style", "section": "Стилевые изменения" },
|
||||
{ "type": "refactor", "section": "Рефакторинг кода" },
|
||||
{ "type": "perf", "section": "Оптимизация производительности" },
|
||||
{ "type": "test", "section": "Тесты" }
|
||||
],
|
||||
"commitUrlFormat": "https://github.com/your-username/drafter/commit/{{hash}}",
|
||||
"compareUrlFormat": "https://github.com/your-username/drafter/compare/{{previousTag}}...{{currentTag}}"
|
||||
}
|
||||
@@ -1,41 +1,114 @@
|
||||
# Drafter (drafter)
|
||||
# Drafter - Редактор шаблонов документов
|
||||
|
||||
Drafter
|
||||
Drafter - это Electron приложение для редактирования шаблонов документов кооперативов. Приложение работает напрямую с файлами cooptypes, позволяя редактировать context, translations и exampleData.
|
||||
|
||||
## Возможности
|
||||
|
||||
- 📝 Редактирование HTML шаблонов с поддержкой Nunjucks
|
||||
- 🌐 Управление переводами (i18n)
|
||||
- 📊 Редактирование примеров данных (object_model)
|
||||
- 💾 Автоматическое сохранение в файлы cooptypes
|
||||
- 🔍 Поиск по шаблонам
|
||||
- 👀 Предварительный просмотр в реальном времени
|
||||
|
||||
## Установка и запуск
|
||||
|
||||
1. Установите зависимости:
|
||||
|
||||
## Install the dependencies
|
||||
```bash
|
||||
yarn
|
||||
# or
|
||||
npm install
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Start the app in development mode (hot-code reloading, error reporting, etc.)
|
||||
2. Настройте путь в `.env` файле:
|
||||
|
||||
```bash
|
||||
quasar dev
|
||||
cp .env-example .env
|
||||
```
|
||||
|
||||
Отредактируйте `.env`:
|
||||
|
||||
### Lint the files
|
||||
```bash
|
||||
yarn lint
|
||||
# or
|
||||
npm run lint
|
||||
```
|
||||
COOPTYPES_REGISTRY_PATH=../cooptypes/src/cooperative/registry
|
||||
```
|
||||
|
||||
3. Запустите в режиме разработки:
|
||||
|
||||
### Format the files
|
||||
```bash
|
||||
yarn format
|
||||
# or
|
||||
npm run format
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
4. Или соберите Electron приложение:
|
||||
|
||||
|
||||
### Build the app for production
|
||||
```bash
|
||||
quasar build
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### Customize the configuration
|
||||
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
drafter/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ ├── TemplatesList.vue # Список шаблонов в левом drawer
|
||||
│ │ ├── htmlEditor.vue # Редактор HTML
|
||||
│ │ ├── modelViewer.vue # Редактор данных
|
||||
│ │ └── htmlPreview.vue # Предварительный просмотр
|
||||
│ ├── services/
|
||||
│ │ └── TemplateService.ts # Сервис для работы с cooptypes файлами
|
||||
│ ├── stores/
|
||||
│ │ └── store.ts # Pinia store
|
||||
│ └── layouts/
|
||||
│ └── MainLayout.vue # Главный layout с drawer
|
||||
├── src-electron/
|
||||
│ ├── electron-main.ts # Главный процесс Electron
|
||||
│ └── electron-preload.ts # Preload скрипт
|
||||
└── .env # Конфигурация путей
|
||||
```
|
||||
|
||||
## Как использовать
|
||||
|
||||
1. **Выбор шаблона**: Используйте левое меню для выбора шаблона из списка
|
||||
2. **Редактирование**:
|
||||
- HTML шаблон редактируется в левой колонке
|
||||
- Данные и переводы - в центральной колонке
|
||||
- Предварительный просмотр - в правой колонке
|
||||
3. **Сохранение**: Нажмите кнопку "Сохранить" рядом с шаблоном в списке
|
||||
4. **Автосохранение**: Изменения сохраняются в `cooptypes/src/cooperative/registry/[template]/index.ts`
|
||||
|
||||
## Формат шаблонов
|
||||
|
||||
Шаблоны используют Nunjucks синтаксис:
|
||||
|
||||
```html
|
||||
<div class="digital-document">
|
||||
<h3>{% trans 'TITLE' %}</h3>
|
||||
<p>{{ coop.city }}, {{ meet.created_at_day }}</p>
|
||||
|
||||
{% for question in questions %}
|
||||
<p>{{ question.number }}. {{ question.title }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
```
|
||||
|
||||
## Структура cooptypes файлов
|
||||
|
||||
Каждый файл шаблона содержит:
|
||||
|
||||
- `context` - HTML шаблон с Nunjucks разметкой
|
||||
- `translations` - Объект с переводами (экспорт `translations`)
|
||||
- `exampleData` - Пример данных для рендеринга (экспорт `exampleData`)
|
||||
|
||||
## Разработка
|
||||
|
||||
Для добавления нового шаблона:
|
||||
|
||||
1. Создайте папку `[id].[Name]/` в `cooptypes/src/cooperative/registry/`
|
||||
2. Добавьте `index.ts` файл с типами и экспортом `context`, `translations`, `exampleData`
|
||||
|
||||
## Технологии
|
||||
|
||||
- **Vue 3** + **TypeScript** - фронтенд
|
||||
- **Quasar Framework** - UI компоненты
|
||||
- **Electron** - десктопное приложение
|
||||
- **Pinia** - управление состоянием
|
||||
- **Nunjucks** - шаблонизатор
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Управление версиями в Drafter
|
||||
|
||||
В этом проекте используется [standard-version](https://github.com/conventional-changelog/standard-version) для автоматического управления версиями и создания тегов Git.
|
||||
|
||||
## Конвенция коммитов
|
||||
|
||||
Для правильной работы standard-version используйте следующий формат сообщений коммитов:
|
||||
|
||||
```
|
||||
<тип>(<область>): <описание>
|
||||
|
||||
[опциональное тело]
|
||||
|
||||
[опциональный footer]
|
||||
```
|
||||
|
||||
Допустимые типы коммитов:
|
||||
|
||||
- **feat**: новая функциональность
|
||||
- **fix**: исправление ошибки
|
||||
- **docs**: изменения в документации
|
||||
- **style**: форматирование, отсутствие semi-colons и т.д.
|
||||
- **refactor**: рефакторинг кода
|
||||
- **perf**: улучшения производительности
|
||||
- **test**: добавление тестов
|
||||
- **chore**: изменения в процессе сборки или вспомогательных инструментах
|
||||
|
||||
## Команды для создания релизов
|
||||
|
||||
Проект имеет следующие скрипты для управления версиями:
|
||||
|
||||
```bash
|
||||
# Стандартный релиз (увеличение патч-версии)
|
||||
pnpm release
|
||||
|
||||
# Увеличение минорной версии (0.1.0 -> 0.2.0)
|
||||
pnpm release:minor
|
||||
|
||||
# Увеличение мажорной версии (1.0.0 -> 2.0.0)
|
||||
pnpm release:major
|
||||
|
||||
# Увеличение патч-версии (1.0.0 -> 1.0.1)
|
||||
pnpm release:patch
|
||||
|
||||
# Создание альфа-версии (1.0.0 -> 1.0.1-alpha.0)
|
||||
pnpm release:alpha
|
||||
|
||||
# Создание бета-версии (1.0.0 -> 1.0.1-beta.0)
|
||||
pnpm release:beta
|
||||
```
|
||||
|
||||
## Процесс релиза
|
||||
|
||||
1. Сделайте необходимые изменения в коде
|
||||
2. Зафиксируйте изменения с помощью правильно оформленных коммитов
|
||||
3. Запустите команду релиза (например, `pnpm release`)
|
||||
4. Пакет автоматически:
|
||||
- Увеличит версию в package.json
|
||||
- Создаст тег в Git
|
||||
- Обновит CHANGELOG.md
|
||||
- Зафиксирует изменения
|
||||
5. Отправьте изменения и теги в репозиторий:
|
||||
```bash
|
||||
git push --follow-tags origin main
|
||||
```
|
||||
6. GitHub Actions автоматически запустит сборку и создаст релиз для всех платформ
|
||||
|
||||
## Ручное создание тега
|
||||
|
||||
Если вам нужно создать тег вручную:
|
||||
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
```
|
||||
+11
-1
@@ -10,12 +10,21 @@
|
||||
"format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore",
|
||||
"test": "echo \"No test specified\" && exit 0",
|
||||
"dev": "quasar dev",
|
||||
"build": "quasar build"
|
||||
"build": "quasar build",
|
||||
"dev:electron": "quasar dev -m electron",
|
||||
"build:electron": "quasar build -m electron",
|
||||
"release": "standard-version",
|
||||
"release:minor": "standard-version --release-as minor",
|
||||
"release:major": "standard-version --release-as major",
|
||||
"release:patch": "standard-version --release-as patch",
|
||||
"release:alpha": "standard-version --prerelease alpha",
|
||||
"release:beta": "standard-version --prerelease beta"
|
||||
},
|
||||
"dependencies": {
|
||||
"@matpool/vue-json-view": "^0.1.8",
|
||||
"@quasar/extras": "^1.16.4",
|
||||
"coopdoc-generator-ts": "^1.0.17",
|
||||
"dotenv": "^16.5.0",
|
||||
"inline-css": "^4.0.2",
|
||||
"jspdf": "^2.5.1",
|
||||
"nunjucks": "^3.2.4",
|
||||
@@ -38,6 +47,7 @@
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"prettier": "^2.5.1",
|
||||
"standard-version": "^9.5.0",
|
||||
"typescript": "^4.5.4",
|
||||
"vite-plugin-checker": "^0.6.4",
|
||||
"vue-tsc": "^1.8.22"
|
||||
|
||||
Generated
+7062
File diff suppressed because it is too large
Load Diff
+8
-3
@@ -5,6 +5,9 @@
|
||||
* the ES6 features that are supported by your Node version. https://node.green/
|
||||
*/
|
||||
|
||||
// Загружаем переменные окружения из .env файла
|
||||
require('dotenv').config();
|
||||
|
||||
// Configuration for your app
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
|
||||
|
||||
@@ -53,7 +56,6 @@ module.exports = configure(function (/* ctx */) {
|
||||
|
||||
// publicPath: '/',
|
||||
// analyze: true,
|
||||
// env: {},
|
||||
// rawDefine: {}
|
||||
// ignorePublicFolder: true,
|
||||
// minify: false,
|
||||
@@ -100,7 +102,7 @@ module.exports = configure(function (/* ctx */) {
|
||||
// directives: [],
|
||||
|
||||
// Quasar plugins
|
||||
plugins: [],
|
||||
plugins: ['Notify'],
|
||||
},
|
||||
|
||||
// animations: 'all', // --- includes all animations
|
||||
@@ -166,7 +168,10 @@ module.exports = configure(function (/* ctx */) {
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
|
||||
electron: {
|
||||
// extendElectronMainConf (esbuildConf)
|
||||
extendElectronMainConf(esbuildConf) {
|
||||
// Убираем зависимость от переменной окружения COOPTYPES_REGISTRY_PATH
|
||||
esbuildConf.define = esbuildConf.define || {};
|
||||
},
|
||||
// extendElectronPreloadConf (esbuildConf)
|
||||
|
||||
inspectPort: 5858,
|
||||
|
||||
Vendored
+21
-1
@@ -2,8 +2,28 @@
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
QUASAR_PUBLIC_FOLDER: string;
|
||||
QUASAR_ELECTRON_PRELOAD: string;
|
||||
APP_URL: string;
|
||||
DEBUGGING?: string;
|
||||
COOPTYPES_REGISTRY_PATH?: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface ElectronAPI {
|
||||
listCooptypeDirectories: () => Promise<string[]>;
|
||||
readCooptypeFile: (templateId: string) => Promise<string>;
|
||||
writeCooptypeFile: (templateId: string, content: string) => Promise<boolean>;
|
||||
|
||||
// Новые методы для работы с путем к реестру
|
||||
setRegistryPath: (path: string) => Promise<boolean>;
|
||||
getRegistryPath: () => Promise<string>;
|
||||
selectDirectory: () => Promise<string | null>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { app, BrowserWindow, ipcMain, dialog } from 'electron';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
const fs = require('fs').promises;
|
||||
|
||||
// needed in case process is undefined under Linux
|
||||
const platform = process.platform || os.platform();
|
||||
|
||||
let mainWindow: BrowserWindow | undefined;
|
||||
|
||||
// Переменная для хранения пути к реестру
|
||||
let registryPath = process.env.COOPTYPES_REGISTRY_PATH || '';
|
||||
|
||||
function createWindow() {
|
||||
/**
|
||||
* Initial window options
|
||||
*/
|
||||
mainWindow = new BrowserWindow({
|
||||
icon: path.resolve(__dirname, 'icons/icon.png'), // tray icon
|
||||
width: 1000,
|
||||
height: 600,
|
||||
width: 1200,
|
||||
height: 800,
|
||||
useContentSize: true,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
// More info: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/electron-preload-script
|
||||
preload: path.resolve(__dirname, process.env.QUASAR_ELECTRON_PRELOAD),
|
||||
preload: path.resolve(
|
||||
__dirname,
|
||||
process.env.QUASAR_ELECTRON_PRELOAD || ''
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.loadURL(process.env.APP_URL);
|
||||
mainWindow.loadURL(process.env.APP_URL || '');
|
||||
|
||||
if (process.env.DEBUGGING) {
|
||||
// if on DEV or Production with debug enabled
|
||||
@@ -40,6 +49,76 @@ function createWindow() {
|
||||
});
|
||||
}
|
||||
|
||||
// Обработчик для установки пути к реестру
|
||||
ipcMain.handle('set-registry-path', async (event, path: string) => {
|
||||
registryPath = path;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Обработчик для получения текущего пути к реестру
|
||||
ipcMain.handle('get-registry-path', async () => {
|
||||
return registryPath;
|
||||
});
|
||||
|
||||
// Обработчик для выбора директории
|
||||
ipcMain.handle('select-directory', async () => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory'],
|
||||
});
|
||||
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
return result.filePaths[0];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// IPC обработчики для работы с cooptypes файлами
|
||||
ipcMain.handle('list-cooptype-directories', async () => {
|
||||
try {
|
||||
if (!registryPath) {
|
||||
throw new Error('Registry path not set');
|
||||
}
|
||||
const items = await fs.readdir(registryPath, { withFileTypes: true });
|
||||
return items
|
||||
.filter((item: any) => item.isDirectory())
|
||||
.map((item: any) => item.name);
|
||||
} catch (error) {
|
||||
console.error('Error listing cooptype directories:', error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('read-cooptype-file', async (event, templateId: string) => {
|
||||
try {
|
||||
if (!registryPath) {
|
||||
throw new Error('Registry path not set');
|
||||
}
|
||||
const filePath = path.join(registryPath, templateId, 'index.ts');
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.error('Error reading cooptype file:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
'write-cooptype-file',
|
||||
async (event, templateId: string, content: string) => {
|
||||
try {
|
||||
if (!registryPath) {
|
||||
throw new Error('Registry path not set');
|
||||
}
|
||||
const filePath = path.join(registryPath, templateId, 'index.ts');
|
||||
await fs.writeFile(filePath, content, 'utf-8');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error writing cooptype file:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
|
||||
@@ -27,4 +27,30 @@
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export {}
|
||||
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Файловые операции для шаблонов
|
||||
readTemplateFile: (fileName: string) =>
|
||||
ipcRenderer.invoke('read-template-file', fileName),
|
||||
writeTemplateFile: (fileName: string, content: string) =>
|
||||
ipcRenderer.invoke('write-template-file', fileName, content),
|
||||
listTemplateFiles: () => ipcRenderer.invoke('list-template-files'),
|
||||
|
||||
// Операции с cooptypes
|
||||
listCooptypeDirectories: () =>
|
||||
ipcRenderer.invoke('list-cooptype-directories'),
|
||||
readCooptypeFile: (templateId: string) =>
|
||||
ipcRenderer.invoke('read-cooptype-file', templateId),
|
||||
writeCooptypeFile: (templateId: string, content: string) =>
|
||||
ipcRenderer.invoke('write-cooptype-file', templateId, content),
|
||||
|
||||
// Операции с путем к реестру
|
||||
setRegistryPath: (path: string) =>
|
||||
ipcRenderer.invoke('set-registry-path', path),
|
||||
getRegistryPath: () => ipcRenderer.invoke('get-registry-path'),
|
||||
selectDirectory: () => ipcRenderer.invoke('select-directory'),
|
||||
});
|
||||
|
||||
export {};
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model="isDialogOpen"
|
||||
persistent
|
||||
transition-show="scale"
|
||||
transition-hide="scale"
|
||||
>
|
||||
<q-card style="min-width: 500px">
|
||||
<q-card-section class="row items-center q-pb-none">
|
||||
<div class="text-h6">Выбор пути к реестру</div>
|
||||
<q-space />
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<p>Укажите путь к директории реестра документов</p>
|
||||
<q-input
|
||||
v-model="registryPathInput"
|
||||
outlined
|
||||
label="Путь к реестру"
|
||||
class="q-mb-md"
|
||||
>
|
||||
<template #after>
|
||||
<q-btn flat icon="folder" @click="selectDirectory" />
|
||||
</template>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
flat
|
||||
label="Отмена"
|
||||
color="primary"
|
||||
@click="closeDialog"
|
||||
v-if="!isFirstLaunch"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
label="Подтвердить"
|
||||
color="primary"
|
||||
@click="confirmPath"
|
||||
:disable="!registryPathInput"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, onMounted } from 'vue';
|
||||
import { useSettingsStore } from 'src/stores/settings';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RegistryPathDialog',
|
||||
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isFirstLaunch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['update:modelValue'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const settingsStore = useSettingsStore();
|
||||
const registryPathInput = ref(settingsStore.registryPath);
|
||||
|
||||
// Вычисляемое свойство для двустороннего связывания диалога
|
||||
const isDialogOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value),
|
||||
});
|
||||
|
||||
// При первом запуске получаем путь из electron
|
||||
onMounted(async () => {
|
||||
if (settingsStore.registryPath === '') {
|
||||
const path = await window.electronAPI.getRegistryPath();
|
||||
if (path) {
|
||||
registryPathInput.value = path;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Выбор директории через диалог
|
||||
const selectDirectory = async () => {
|
||||
const selectedPath = await window.electronAPI.selectDirectory();
|
||||
if (selectedPath) {
|
||||
registryPathInput.value = selectedPath;
|
||||
}
|
||||
};
|
||||
|
||||
// Подтверждение выбранного пути
|
||||
const confirmPath = async () => {
|
||||
if (registryPathInput.value) {
|
||||
await window.electronAPI.setRegistryPath(registryPathInput.value);
|
||||
settingsStore.setRegistryPath(registryPathInput.value);
|
||||
isDialogOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Закрытие диалога
|
||||
const closeDialog = () => {
|
||||
isDialogOpen.value = false;
|
||||
};
|
||||
|
||||
return {
|
||||
isDialogOpen,
|
||||
registryPathInput,
|
||||
selectDirectory,
|
||||
confirmPath,
|
||||
closeDialog,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<q-btn-dropdown
|
||||
flat
|
||||
no-caps
|
||||
:label="shortPath"
|
||||
icon="folder"
|
||||
class="registry-path-dropdown"
|
||||
>
|
||||
<q-card class="no-shadow">
|
||||
<q-card-section>
|
||||
<div class="text-subtitle2">Текущий путь к реестру</div>
|
||||
<div class="registry-path">{{ registryPath }}</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Изменить" color="primary" @click="openDialog" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-btn-dropdown>
|
||||
|
||||
<registry-path-dialog v-model="showDialog" :is-first-launch="false" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed } from 'vue';
|
||||
import { useSettingsStore } from 'src/stores/settings';
|
||||
import RegistryPathDialog from './RegistryPathDialog.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RegistryPathSetter',
|
||||
|
||||
components: {
|
||||
RegistryPathDialog,
|
||||
},
|
||||
|
||||
setup() {
|
||||
const settingsStore = useSettingsStore();
|
||||
const showDialog = ref(false);
|
||||
|
||||
const registryPath = computed(() => settingsStore.registryPath);
|
||||
|
||||
// Сокращенный путь для отображения в кнопке
|
||||
const shortPath = computed(() => {
|
||||
const path = registryPath.value;
|
||||
if (!path) return 'Путь не выбран';
|
||||
|
||||
const parts = path.split('/');
|
||||
if (parts.length <= 2) return path;
|
||||
|
||||
return `.../${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
|
||||
});
|
||||
|
||||
const openDialog = () => {
|
||||
showDialog.value = true;
|
||||
};
|
||||
|
||||
return {
|
||||
registryPath,
|
||||
shortPath,
|
||||
showDialog,
|
||||
openDialog,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.registry-path {
|
||||
word-break: break-all;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.registry-path-dropdown {
|
||||
max-width: 300px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div class="templates-list">
|
||||
<div class="q-pa-md">
|
||||
<q-input
|
||||
v-model="searchQuery"
|
||||
placeholder="Поиск шаблонов..."
|
||||
outlined
|
||||
dense
|
||||
clearable
|
||||
@input="filterTemplates"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<q-list>
|
||||
<q-item-label header class="text-grey-8">
|
||||
Шаблоны ({{ filteredTemplates.length }})
|
||||
</q-item-label>
|
||||
|
||||
<q-item
|
||||
v-for="template in sortedTemplates"
|
||||
:key="template.id"
|
||||
clickable
|
||||
v-ripple
|
||||
:active="currentTemplate?.id === template.id"
|
||||
@click="selectTemplate(template)"
|
||||
class="template-item"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-weight-medium">
|
||||
{{ template.name }}
|
||||
</q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
class="text-grey-6"
|
||||
v-if="loadedTitles[template.id]"
|
||||
>
|
||||
{{ loadedTitles[template.id] }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item v-if="filteredTemplates.length === 0">
|
||||
<q-item-section>
|
||||
<q-item-label class="text-grey-6"> Шаблоны не найдены </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<div class="q-pa-md" v-if="isLoading">
|
||||
<q-skeleton height="50px" class="q-mb-sm" v-for="n in 5" :key="n" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, reactive } from 'vue';
|
||||
import { useAppState } from 'src/stores/store';
|
||||
import {
|
||||
TemplateService,
|
||||
type TemplateFile,
|
||||
} from 'src/services/TemplateService';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
const $q = useQuasar();
|
||||
const appState = useAppState();
|
||||
const templateService = new TemplateService();
|
||||
|
||||
const searchQuery = ref('');
|
||||
const isLoading = ref(false);
|
||||
const loadedTitles = reactive<Record<string, string>>({});
|
||||
|
||||
const currentTemplate = computed(() => appState.currentTemplate);
|
||||
const templates = computed(() => appState.templates);
|
||||
|
||||
// Определяем события, которые компонент может испускать
|
||||
const emit = defineEmits(['template-selected']);
|
||||
|
||||
const filteredTemplates = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return templates.value;
|
||||
}
|
||||
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return templates.value.filter(
|
||||
(template) =>
|
||||
template.name.toLowerCase().includes(query) ||
|
||||
template.fileName.toLowerCase().includes(query) ||
|
||||
(loadedTitles[template.id] &&
|
||||
loadedTitles[template.id].toLowerCase().includes(query))
|
||||
);
|
||||
});
|
||||
|
||||
// Сортировка шаблонов по порядковому номеру
|
||||
const sortedTemplates = computed(() => {
|
||||
return [...filteredTemplates.value].sort((a, b) => {
|
||||
// Извлекаем числовой номер из начала названия
|
||||
const getNumber = (name: string) => {
|
||||
const match = name.match(/^(\d+)\./);
|
||||
return match ? parseInt(match[1], 10) : 0;
|
||||
};
|
||||
|
||||
const numA = getNumber(a.name);
|
||||
const numB = getNumber(b.name);
|
||||
|
||||
return numA - numB;
|
||||
});
|
||||
});
|
||||
|
||||
const selectTemplate = async (template: TemplateFile) => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// Загружаем полные данные шаблона
|
||||
const fullTemplate = await templateService.loadTemplate(template.fileName);
|
||||
if (fullTemplate) {
|
||||
// Сохраняем title шаблона в нашем локальном хранилище
|
||||
if (fullTemplate.title && fullTemplate.title !== template.name) {
|
||||
loadedTitles[template.id] = fullTemplate.title;
|
||||
}
|
||||
|
||||
appState.setCurrentTemplate(fullTemplate);
|
||||
appState.parseContext();
|
||||
// Испускаем событие выбора шаблона
|
||||
emit('template-selected');
|
||||
} else {
|
||||
throw new Error('Failed to load template');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error selecting template:', error);
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Ошибка при загрузке шаблона',
|
||||
position: 'top',
|
||||
});
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const filterTemplates = () => {
|
||||
// Логика фильтрации реализована через computed свойство
|
||||
};
|
||||
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const templatesList = await templateService.loadTemplatesList();
|
||||
appState.setTemplates(templatesList);
|
||||
|
||||
// Загружаем titles для всех шаблонов в фоновом режиме
|
||||
setTimeout(() => {
|
||||
loadTemplateTitles(templatesList);
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('Error loading templates:', error);
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Ошибка при загрузке списка шаблонов',
|
||||
position: 'top',
|
||||
});
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Загрузка titles шаблонов в фоновом режиме
|
||||
const loadTemplateTitles = async (templatesList: TemplateFile[]) => {
|
||||
for (const template of templatesList) {
|
||||
try {
|
||||
const fullTemplate = await templateService.loadTemplate(
|
||||
template.fileName
|
||||
);
|
||||
if (
|
||||
fullTemplate &&
|
||||
fullTemplate.title &&
|
||||
fullTemplate.title !== template.name
|
||||
) {
|
||||
loadedTitles[template.id] = fullTemplate.title;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading title for template ${template.id}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
if (!appState.isTemplateListLoaded) {
|
||||
await loadTemplates();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.templates-list {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.template-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.template-item.q-item--active {
|
||||
background-color: rgba(25, 118, 210, 0.12);
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +1,42 @@
|
||||
<template>
|
||||
<q-card flat class="q-pa-sm">
|
||||
<p class="text-center">Редактор</p>
|
||||
<q-input
|
||||
@change="state.parseContext"
|
||||
v-model="state.context"
|
||||
type="textarea"
|
||||
rows="50"
|
||||
class="q-pa-xs"
|
||||
style="word-break: break-all"
|
||||
></q-input>
|
||||
<q-card flat class="editor-card" ref="cardEl">
|
||||
<q-banner class="bg-primary text-white no-border-radius q-px-md q-py-sm">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="code" />
|
||||
</template>
|
||||
<q-badge class="q-ml-md" color="white" text-color="primary">
|
||||
Шаблон
|
||||
</q-badge>
|
||||
</q-banner>
|
||||
<div
|
||||
class="editor-content"
|
||||
contenteditable="plaintext-only"
|
||||
@input="updateContent"
|
||||
ref="contentEl"
|
||||
></div>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.line-wrap {
|
||||
.editor-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
min-height: 70vh;
|
||||
outline: none;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.no-border-radius {
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,5 +46,33 @@ defineOptions({
|
||||
});
|
||||
|
||||
import { useAppState } from 'src/stores/store';
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
|
||||
const state = useAppState();
|
||||
const contentEl = ref<HTMLDivElement | null>(null);
|
||||
const cardEl = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const updateContent = () => {
|
||||
if (contentEl.value) {
|
||||
state.context = contentEl.value.innerText;
|
||||
state.parseContext();
|
||||
}
|
||||
};
|
||||
|
||||
// Обновляем содержимое редактора при изменении state.context извне
|
||||
watch(
|
||||
() => state.context,
|
||||
(newValue) => {
|
||||
if (contentEl.value && contentEl.value.innerText !== newValue) {
|
||||
contentEl.value.innerText = newValue;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Инициализация редактора при монтировании
|
||||
onMounted(() => {
|
||||
if (contentEl.value && state.context) {
|
||||
contentEl.value.innerText = state.context;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
<template>
|
||||
<q-card flat>
|
||||
<p class="text-center">Предпросмотр</p>
|
||||
<pre v-html="state.html" class="line-wrap"></pre>
|
||||
<q-card flat class="preview-card">
|
||||
<q-banner class="bg-green text-white no-border-radius q-px-md q-py-sm">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="visibility" />
|
||||
</template>
|
||||
<q-badge class="q-ml-md" color="white" text-color="green">
|
||||
Предпросмотр
|
||||
</q-badge>
|
||||
</q-banner>
|
||||
<div class="preview-content" v-html="state.html"></div>
|
||||
</q-card>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.line-wrap {
|
||||
.preview-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.no-border-radius {
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,6 +39,5 @@ defineOptions({
|
||||
});
|
||||
|
||||
import { useAppState } from 'src/stores/store';
|
||||
const state = useAppState()
|
||||
|
||||
const state = useAppState();
|
||||
</script>
|
||||
|
||||
@@ -1,80 +1,91 @@
|
||||
<!-- eslint-disable @typescript-eslint/no-explicit-any -->
|
||||
<template>
|
||||
<q-card flat>
|
||||
<p class="text-center">Переменные и переводы</p>
|
||||
<q-tree
|
||||
:nodes="nodes"
|
||||
node-key="id"
|
||||
:label-key="labelKey"
|
||||
:children-key="childrenKey"
|
||||
:default-expanded="defaultExpanded"
|
||||
>
|
||||
<template v-slot:default-body="{ node }">
|
||||
<div v-if="node.isAddButton" class="q-pa-xs">
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="primary"
|
||||
icon="add"
|
||||
:label="node.label"
|
||||
@click="addArrayItemByPath(node.arrayPath)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="!node.children" class="q-pa-xs">
|
||||
<q-input
|
||||
type="textarea"
|
||||
rows="3"
|
||||
dense
|
||||
filled
|
||||
v-model="node[displayKey]"
|
||||
@update:model-value="updateValue(node, $event)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="node.isArray" class="q-pa-xs">
|
||||
<div class="row items-center q-mb-sm">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="primary"
|
||||
icon="add"
|
||||
label="Добавить элемент"
|
||||
@click="addArrayItem(node)"
|
||||
/>
|
||||
</div>
|
||||
<q-card flat class="model-card">
|
||||
<q-banner class="bg-secondary text-white no-border-radius q-px-md q-py-sm">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="data_object" />
|
||||
</template>
|
||||
<q-badge class="q-ml-md" color="white" text-color="secondary">
|
||||
Данные
|
||||
</q-badge>
|
||||
</q-banner>
|
||||
<div class="model-content">
|
||||
<q-tree
|
||||
:nodes="nodes"
|
||||
node-key="id"
|
||||
:label-key="labelKey"
|
||||
:children-key="childrenKey"
|
||||
:default-expanded="defaultExpanded"
|
||||
>
|
||||
<template v-slot:default-body="{ node }">
|
||||
<div v-if="node.isAddButton" class="q-pa-xs">
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="primary"
|
||||
icon="add"
|
||||
:label="node.label"
|
||||
@click="addArrayItemByPath(node.arrayPath)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, index) in node.arrayItems"
|
||||
:key="index"
|
||||
class="q-mb-md q-pa-sm"
|
||||
style="border: 1px solid #e0e0e0; border-radius: 4px"
|
||||
>
|
||||
<div v-else-if="!node.children" class="q-pa-xs">
|
||||
<q-input
|
||||
type="textarea"
|
||||
rows="3"
|
||||
dense
|
||||
filled
|
||||
v-model="node[displayKey]"
|
||||
@update:model-value="updateValue(node, $event)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="node.isArray" class="q-pa-xs">
|
||||
<div class="row items-center q-mb-sm">
|
||||
<div class="col">
|
||||
<strong>{{ node.label }} [{{ index }}]</strong>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="negative"
|
||||
icon="delete"
|
||||
@click="removeArrayItem(node, index)"
|
||||
color="primary"
|
||||
icon="add"
|
||||
label="Добавить элемент"
|
||||
@click="addArrayItem(node)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(value, key) in item" :key="key" class="q-mb-sm">
|
||||
<q-input
|
||||
:label="String(key)"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
dense
|
||||
filled
|
||||
v-model="item[key]"
|
||||
@change="updateArrayValue(node, index, String(key), item[key])"
|
||||
/>
|
||||
<div
|
||||
v-for="(item, index) in node.arrayItems"
|
||||
:key="index"
|
||||
class="q-mb-md q-pa-sm"
|
||||
style="border: 1px solid #e0e0e0; border-radius: 4px"
|
||||
>
|
||||
<div class="row items-center q-mb-sm">
|
||||
<div class="col">
|
||||
<strong>{{ node.label }} [{{ index }}]</strong>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="negative"
|
||||
icon="delete"
|
||||
@click="removeArrayItem(node, index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(value, key) in item" :key="key" class="q-mb-sm">
|
||||
<q-input
|
||||
:label="String(key)"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
dense
|
||||
filled
|
||||
v-model="item[key]"
|
||||
@change="
|
||||
updateArrayValue(node, index, String(key), item[key])
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</q-tree>
|
||||
</template>
|
||||
</q-tree>
|
||||
</div>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
@@ -419,3 +430,22 @@ watchEffect(() => {
|
||||
nodes.value = generateTree();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.model-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.model-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.no-border-radius {
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
+129
-3
@@ -1,12 +1,138 @@
|
||||
<template>
|
||||
<q-layout view="lHh Lpr lFf">
|
||||
<q-page-container>
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
<q-header elevated>
|
||||
<q-toolbar>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
round
|
||||
icon="menu"
|
||||
aria-label="Menu"
|
||||
@click="toggleLeftDrawer"
|
||||
/>
|
||||
|
||||
<q-toolbar-title>
|
||||
{{ currentTemplate ? currentTemplate.name : 'Редактор шаблонов' }}
|
||||
</q-toolbar-title>
|
||||
|
||||
<q-space />
|
||||
|
||||
<!-- Компонент для отображения и изменения пути к реестру -->
|
||||
<registry-path-setter />
|
||||
|
||||
<q-btn
|
||||
v-if="currentTemplate"
|
||||
flat
|
||||
dense
|
||||
round
|
||||
icon="save"
|
||||
aria-label="Сохранить"
|
||||
color="positive"
|
||||
@click="saveCurrentTemplate"
|
||||
>
|
||||
<q-tooltip>Сохранить шаблон</q-tooltip>
|
||||
</q-btn>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-splitter v-model="splitterModel" :limits="[0, 50]" class="full-height">
|
||||
<template v-slot:before>
|
||||
<div v-if="leftDrawerOpen" class="bg-grey-1 full-height q-pa-sm">
|
||||
<TemplatesList @template-selected="closeDrawer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:after>
|
||||
<q-page-container>
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
</template>
|
||||
</q-splitter>
|
||||
|
||||
<!-- Диалог выбора пути при первом запуске -->
|
||||
<registry-path-dialog v-model="showPathDialog" :is-first-launch="true" />
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useAppState } from 'src/stores/store';
|
||||
import { useSettingsStore } from 'src/stores/settings';
|
||||
import { useQuasar } from 'quasar';
|
||||
import TemplatesList from 'src/components/TemplatesList.vue';
|
||||
import RegistryPathSetter from 'src/components/RegistryPathSetter.vue';
|
||||
import RegistryPathDialog from 'src/components/RegistryPathDialog.vue';
|
||||
import { TemplateService } from 'src/services/TemplateService';
|
||||
|
||||
const $q = useQuasar();
|
||||
const appState = useAppState();
|
||||
const settingsStore = useSettingsStore();
|
||||
const templateService = new TemplateService();
|
||||
|
||||
const leftDrawerOpen = ref(false);
|
||||
const showPathDialog = ref(false);
|
||||
const splitterModel = ref(0); // Начальное значение 0 (закрыто)
|
||||
const savedSplitterWidth = ref(20); // Сохраняем ширину для восстановления
|
||||
|
||||
const currentTemplate = computed(() => appState.currentTemplate);
|
||||
|
||||
// Проверка при запуске, нужно ли показать диалог выбора пути
|
||||
onMounted(() => {
|
||||
if (settingsStore.isFirstLaunch || !settingsStore.registryPath) {
|
||||
showPathDialog.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Отслеживаем изменение состояния бокового меню
|
||||
watch(leftDrawerOpen, (isOpen) => {
|
||||
if (isOpen) {
|
||||
// Открываем боковую панель
|
||||
splitterModel.value = savedSplitterWidth.value;
|
||||
} else {
|
||||
// Сохраняем текущую ширину перед закрытием
|
||||
if (splitterModel.value > 0) {
|
||||
savedSplitterWidth.value = splitterModel.value;
|
||||
}
|
||||
// Закрываем боковую панель
|
||||
splitterModel.value = 0;
|
||||
}
|
||||
});
|
||||
|
||||
const toggleLeftDrawer = () => {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value;
|
||||
};
|
||||
|
||||
const closeDrawer = () => {
|
||||
leftDrawerOpen.value = false;
|
||||
};
|
||||
|
||||
const saveCurrentTemplate = async () => {
|
||||
if (!currentTemplate.value) return;
|
||||
|
||||
try {
|
||||
// Обновляем данные шаблона из store
|
||||
appState.updateCurrentTemplate();
|
||||
|
||||
const success = await templateService.saveTemplate(currentTemplate.value);
|
||||
if (success) {
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: `Шаблон "${currentTemplate.value.name}" сохранен`,
|
||||
position: 'top',
|
||||
});
|
||||
} else {
|
||||
throw new Error('Save failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving template:', error);
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Ошибка при сохранении шаблона',
|
||||
position: 'top',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
defineOptions({
|
||||
name: 'MainLayout',
|
||||
});
|
||||
|
||||
+50
-20
@@ -1,39 +1,69 @@
|
||||
<template>
|
||||
<q-page>
|
||||
<div class="row">
|
||||
<updateButton class="col-4"/>
|
||||
<templateUploader class="col-4"/>
|
||||
<downloadButton class="col-4"/>
|
||||
<div v-if="!currentTemplate" class="full-height flex flex-center">
|
||||
<div class="text-center">
|
||||
<q-icon name="description" size="4rem" color="grey-5" />
|
||||
<h5 class="text-grey-6 q-mt-md q-mb-md">
|
||||
Выберите шаблон для редактирования
|
||||
</h5>
|
||||
<p class="text-grey-5">
|
||||
Используйте меню слева для выбора шаблона из списка
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" >
|
||||
<htmlEditor class="col-4 q-pa-md scrolled" />
|
||||
<modelViewer class="col-4 q-pa-md scrolled"/>
|
||||
<htmlPreview class="col-4 q-pa-md scrolled"/>
|
||||
|
||||
<div v-else class="q-pa-sm">
|
||||
<div class="editor-container">
|
||||
<q-splitter v-model="editorSplit" class="full-height">
|
||||
<template v-slot:before>
|
||||
<htmlEditor class="q-pa-md" />
|
||||
</template>
|
||||
|
||||
<template v-slot:after>
|
||||
<q-splitter v-model="rightSplit" class="full-height">
|
||||
<template v-slot:before>
|
||||
<modelViewer class="q-pa-md" />
|
||||
</template>
|
||||
|
||||
<template v-slot:after>
|
||||
<htmlPreview class="q-pa-md" />
|
||||
</template>
|
||||
</q-splitter>
|
||||
</template>
|
||||
</q-splitter>
|
||||
</div>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// import { ref } from 'vue';
|
||||
// import { Meta } from 'components/models';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useAppState } from 'src/stores/store';
|
||||
import htmlPreview from 'src/components/htmlPreview.vue';
|
||||
import updateButton from 'src/components/updateButton.vue';
|
||||
import downloadButton from 'src/components/downloadButton.vue';
|
||||
import templateUploader from 'src/components/templateUploader.vue';
|
||||
import htmlEditor from 'src/components/htmlEditor.vue';
|
||||
import modelViewer from 'src/components/modelViewer.vue';
|
||||
|
||||
const appState = useAppState();
|
||||
const editorSplit = ref(33); // 33% для левой панели
|
||||
const rightSplit = ref(50); // 50% для средней панели от оставшегося пространства
|
||||
|
||||
const currentTemplate = computed(() => appState.currentTemplate);
|
||||
|
||||
defineOptions({
|
||||
name: 'IndexPage',
|
||||
});
|
||||
|
||||
// const meta = ref<Meta>({
|
||||
// totalCount: 1200,
|
||||
// });
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.scrolled{
|
||||
max-height: 90vh;
|
||||
overflow-y: scroll;
|
||||
.editor-container {
|
||||
height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
.full-height {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.q-splitter :deep(.q-splitter__panel) {
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
+479
-92
@@ -9,55 +9,493 @@ export interface TemplateFile {
|
||||
}
|
||||
|
||||
export class TemplateService {
|
||||
private registryPath: string;
|
||||
private cooptypesPath: string;
|
||||
|
||||
constructor() {
|
||||
this.registryPath =
|
||||
import.meta.env.VITE_TEMPLATES_REGISTRY_PATH || '../registry';
|
||||
this.cooptypesPath =
|
||||
import.meta.env.VITE_COOPTYPES_REGISTRY_PATH ||
|
||||
'../cooptypes/src/cooperative/registry';
|
||||
}
|
||||
|
||||
async loadTemplatesList(): Promise<TemplateFile[]> {
|
||||
// В реальном приложении здесь будет запрос к API
|
||||
// Пока что заглушка для тестирования
|
||||
const templates = [
|
||||
'1.walletProgramAgreement.json',
|
||||
'100.participantApplication.json',
|
||||
'1000.investAgreement.json',
|
||||
'1001.investByResultStatement.json',
|
||||
'1002.investByResultAct.json',
|
||||
'1005.investByMoneyStatement.json',
|
||||
'101.selectBranchStatement.json',
|
||||
'1010.investMembershipConvertation.json',
|
||||
'2.regulationElectronicSignature.json',
|
||||
'3.privacyPolicy.json',
|
||||
'300.annualGeneralMeetingAgenda.json',
|
||||
'301.annualGeneralMeetingSovietDecision.json',
|
||||
'302.annualGeneralMeetingNotification.json',
|
||||
'303.annualGeneralMeetingVotingBallot.json',
|
||||
'304.annualGeneralMeetingDecision.json',
|
||||
'4.userAgreement.json',
|
||||
'50.CoopenomicsAgreement.json',
|
||||
'501.decisionOfParticipantApplication.json',
|
||||
'599.projectFreeDecision.json',
|
||||
'600.freeDecision.json',
|
||||
'700.assetContributionStatement.json',
|
||||
'701.assetContributionDecision.json',
|
||||
'702.assetContributionAct.json',
|
||||
'800.returnByAssetStatement.json',
|
||||
'801.returnByAssetDecision.json',
|
||||
'802.returnByAssetAct.json',
|
||||
];
|
||||
try {
|
||||
// Проверяем, доступен ли Electron API
|
||||
if (window.electronAPI) {
|
||||
const templateDirs = await window.electronAPI.listCooptypeDirectories();
|
||||
return templateDirs.map((dirName) => {
|
||||
const id = dirName;
|
||||
const name = this.formatTemplateName(dirName);
|
||||
const fileName = `${dirName}.json`; // Для совместимости
|
||||
|
||||
return templates.map((fileName) => {
|
||||
const id = fileName.replace('.json', '');
|
||||
const name = this.formatTemplateName(fileName);
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
title: name,
|
||||
fileName,
|
||||
context: '',
|
||||
translation: {},
|
||||
exampleData: {},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Fallback для веб-версии
|
||||
return this.getHardcodedTemplatesList();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading templates list:', error);
|
||||
return this.getHardcodedTemplatesList();
|
||||
}
|
||||
}
|
||||
|
||||
async loadTemplate(fileName: string): Promise<TemplateFile | null> {
|
||||
try {
|
||||
const templateId = fileName.replace('.json', '');
|
||||
console.log('🔄 Loading template:', { fileName, templateId });
|
||||
|
||||
// Проверяем, доступен ли Electron API
|
||||
if (window.electronAPI) {
|
||||
// Загружаем данные из cooptypes
|
||||
console.log('📁 Reading cooptype file via Electron API...');
|
||||
const cooptypeContent = await window.electronAPI.readCooptypeFile(
|
||||
templateId
|
||||
);
|
||||
console.log(
|
||||
'📄 Raw cooptype content preview:',
|
||||
cooptypeContent.substring(0, 300) + '...'
|
||||
);
|
||||
|
||||
const cooptypeData = this.parseCooptypeFile(cooptypeContent);
|
||||
console.log('🔍 Parsed cooptype data:', {
|
||||
contextLength: cooptypeData.context?.length || 0,
|
||||
translationsKeys: Object.keys(cooptypeData.translations || {}),
|
||||
exampleDataKeys: Object.keys(cooptypeData.exampleData || {}),
|
||||
hasExampleData: !!(
|
||||
cooptypeData.exampleData &&
|
||||
Object.keys(cooptypeData.exampleData).length > 0
|
||||
),
|
||||
});
|
||||
|
||||
const context = cooptypeData.context || '';
|
||||
const translation = cooptypeData.translations?.ru || {};
|
||||
const exampleData = cooptypeData.exampleData || {};
|
||||
// Используем извлеченный title или формируем из имени шаблона
|
||||
const title = cooptypeData.title || this.formatTemplateName(templateId);
|
||||
|
||||
console.log('✅ Final template data:', {
|
||||
id: templateId,
|
||||
contextLength: context.length,
|
||||
translationKeys: Object.keys(translation),
|
||||
exampleDataKeys: Object.keys(exampleData),
|
||||
exampleDataSample:
|
||||
JSON.stringify(exampleData).substring(0, 200) + '...',
|
||||
});
|
||||
|
||||
return {
|
||||
id: templateId,
|
||||
name: this.formatTemplateName(templateId),
|
||||
title: title,
|
||||
fileName,
|
||||
context,
|
||||
translation,
|
||||
exampleData,
|
||||
};
|
||||
} else {
|
||||
// Fallback для веб-версии - используем fetch
|
||||
console.log('🌐 Fallback: using fetch for web version...');
|
||||
const response = await fetch(`/api/cooptypes/${templateId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Template not found');
|
||||
}
|
||||
|
||||
const data = await response.text();
|
||||
const cooptypeData = this.parseCooptypeFile(data);
|
||||
const title = cooptypeData.title || this.formatTemplateName(templateId);
|
||||
|
||||
return {
|
||||
id: templateId,
|
||||
name: this.formatTemplateName(templateId),
|
||||
title: title,
|
||||
fileName,
|
||||
context: cooptypeData.context || '',
|
||||
translation: cooptypeData.translations?.ru || {},
|
||||
exampleData: cooptypeData.exampleData || {},
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading template:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async saveTemplate(template: TemplateFile): Promise<boolean> {
|
||||
try {
|
||||
// Проверяем, доступен ли Electron API
|
||||
if (window.electronAPI) {
|
||||
// Обновляем только файл в cooptypes
|
||||
await this.updateCooptypeFile(template);
|
||||
return true;
|
||||
} else {
|
||||
// Fallback для веб-версии
|
||||
const response = await fetch(`/api/cooptypes/${template.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
context: template.context,
|
||||
translation: template.translation,
|
||||
exampleData: template.exampleData,
|
||||
}),
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving template:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private parseCooptypeFile(content: string): any {
|
||||
try {
|
||||
// Извлекаем title из файла
|
||||
const titleMatch = content.match(/export const title = ['"`](.*?)['"`]/);
|
||||
let title = '';
|
||||
if (titleMatch) {
|
||||
title = titleMatch[1]
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\\t/g, '\t')
|
||||
.replace(/\\'/g, "'")
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\\\/g, '\\');
|
||||
}
|
||||
|
||||
// Извлекаем context с учетом экранированных символов и переносов строк
|
||||
const contextMatch = content.match(
|
||||
/export const context = ['"`]([\s\S]*?)['"`](?=\s*export|\s*$)/
|
||||
);
|
||||
let context = '';
|
||||
if (contextMatch) {
|
||||
// Правильно обрабатываем экранированные символы
|
||||
context = contextMatch[1]
|
||||
.replace(/\\n/g, '\n') // Заменяем \\n на реальные переносы строк
|
||||
.replace(/\\t/g, '\t') // Заменяем \\t на табуляцию
|
||||
.replace(/\\'/g, "'") // Заменяем \\' на одинарные кавычки
|
||||
.replace(/\\"/g, '"') // Заменяем \\" на двойные кавычки
|
||||
.replace(/\\\\/g, '\\'); // Заменяем \\\\ на обратные слеши
|
||||
}
|
||||
|
||||
// Функция для извлечения содержимого объекта с учетом вложенных скобок
|
||||
const extractObjectContent = (
|
||||
text: string,
|
||||
startPattern: string
|
||||
): string | null => {
|
||||
const startIndex = text.indexOf(startPattern);
|
||||
if (startIndex === -1) return null;
|
||||
|
||||
const objectStart = text.indexOf('{', startIndex);
|
||||
if (objectStart === -1) return null;
|
||||
|
||||
let braceCount = 0;
|
||||
let current = objectStart;
|
||||
|
||||
while (current < text.length) {
|
||||
const char = text[current];
|
||||
if (char === '{') braceCount++;
|
||||
if (char === '}') braceCount--;
|
||||
|
||||
if (braceCount === 0) {
|
||||
// Найдена закрывающая скобка объекта
|
||||
return text.substring(objectStart + 1, current);
|
||||
}
|
||||
current++;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Извлекаем translations с более надежным подходом
|
||||
let translations = {};
|
||||
const translationsContent = extractObjectContent(
|
||||
content,
|
||||
'export const translations = {'
|
||||
);
|
||||
if (translationsContent) {
|
||||
try {
|
||||
// Создаем временную функцию для безопасного выполнения кода
|
||||
const translationsCode = `return {${translationsContent}}`;
|
||||
const func = new Function(translationsCode);
|
||||
translations = func();
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'Could not parse translations, trying alternative method:',
|
||||
e
|
||||
);
|
||||
// Альтернативный метод - ищем ru секцию
|
||||
const ruMatch = translationsContent.match(
|
||||
/ru:\s*\{([\s\S]*?)\}(?=\s*,?\s*[a-zA-Z_]|\s*$)/
|
||||
);
|
||||
if (ruMatch) {
|
||||
try {
|
||||
const ruCode = `return {${ruMatch[1]}}`;
|
||||
const ruFunc = new Function(ruCode);
|
||||
translations = { ru: ruFunc() };
|
||||
} catch (e2) {
|
||||
console.warn('Alternative parsing also failed:', e2);
|
||||
console.log('Failed ru content:', ruMatch[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Извлекаем exampleData если есть
|
||||
let exampleData = {};
|
||||
const exampleDataContent = extractObjectContent(
|
||||
content,
|
||||
'export const exampleData = {'
|
||||
);
|
||||
if (exampleDataContent) {
|
||||
try {
|
||||
const exampleDataCode = `return {${exampleDataContent}}`;
|
||||
const func = new Function(exampleDataCode);
|
||||
exampleData = func();
|
||||
} catch (e) {
|
||||
console.warn('Could not parse exampleData:', e);
|
||||
console.log(
|
||||
'Failed exampleData content:',
|
||||
exampleDataContent.substring(0, 200) + '...'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Parsed cooptype file:', {
|
||||
contextLength: context.length,
|
||||
translationsKeys: Object.keys(translations),
|
||||
exampleDataKeys: Object.keys(exampleData),
|
||||
contextPreview:
|
||||
context.substring(0, 200) + (context.length > 200 ? '...' : ''),
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
context,
|
||||
translations,
|
||||
exampleData,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error parsing cooptype file:', error);
|
||||
return {
|
||||
title: '',
|
||||
context: '',
|
||||
translations: {},
|
||||
exampleData: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async updateCooptypeFile(template: TemplateFile): Promise<void> {
|
||||
try {
|
||||
if (!window.electronAPI) return;
|
||||
|
||||
// Читаем существующий файл cooptype
|
||||
let existingContent = await window.electronAPI.readCooptypeFile(
|
||||
template.id
|
||||
);
|
||||
|
||||
// Максимально простой и надежный подход - используем грубую замену текста
|
||||
// на основе идентификации строк начала и конца блока контекста
|
||||
|
||||
// Находим начало блока контекста
|
||||
const contextStartPattern = 'export const context =';
|
||||
const contextStartIndex = existingContent.indexOf(contextStartPattern);
|
||||
|
||||
if (contextStartIndex === -1) {
|
||||
console.error('Блок context не найден в файле');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ищем первый символ после "export const context ="
|
||||
let quoteStartIndex = contextStartIndex + contextStartPattern.length;
|
||||
// Пропускаем пробелы
|
||||
while (existingContent[quoteStartIndex] === ' ') {
|
||||
quoteStartIndex++;
|
||||
}
|
||||
|
||||
// Определяем тип кавычки
|
||||
const quoteChar = existingContent[quoteStartIndex];
|
||||
|
||||
if (quoteChar !== "'" && quoteChar !== '"' && quoteChar !== '`') {
|
||||
console.error('Не удалось определить тип кавычки в блоке context');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ищем конец блока (закрывающую кавычку с учетом экранирования)
|
||||
let quoteEndIndex = quoteStartIndex + 1;
|
||||
let escaped = false;
|
||||
|
||||
// Поиск закрывающей кавычки с учетом вложенных кавычек и экранирования
|
||||
while (quoteEndIndex < existingContent.length) {
|
||||
const char = existingContent[quoteEndIndex];
|
||||
|
||||
if (char === '\\') {
|
||||
escaped = !escaped; // Переключаем состояние экранирования
|
||||
} else if (char === quoteChar && !escaped) {
|
||||
break; // Нашли закрывающую кавычку
|
||||
} else {
|
||||
escaped = false; // Сбрасываем состояние экранирования для других символов
|
||||
}
|
||||
|
||||
quoteEndIndex++;
|
||||
}
|
||||
|
||||
if (quoteEndIndex >= existingContent.length) {
|
||||
console.error('Не удалось найти конец блока context');
|
||||
return;
|
||||
}
|
||||
|
||||
// Формируем новый блок контекста
|
||||
const newContextBlock = `${contextStartPattern} \`${template.context}\``;
|
||||
|
||||
// Заменяем старый блок контекста на новый
|
||||
const beforeContext = existingContent.substring(0, contextStartIndex);
|
||||
const afterContext = existingContent.substring(quoteEndIndex + 1);
|
||||
|
||||
// Собираем обновленное содержимое
|
||||
existingContent = beforeContext + newContextBlock + afterContext;
|
||||
|
||||
// Обновляем translations и exampleData (используя простой подход с поиском маркеров)
|
||||
|
||||
// Обновляем translations
|
||||
const translationsStartPattern = 'export const translations =';
|
||||
const translationsStartIndex = existingContent.indexOf(
|
||||
translationsStartPattern
|
||||
);
|
||||
|
||||
if (translationsStartIndex !== -1) {
|
||||
const translationsBracketIndex = existingContent.indexOf(
|
||||
'{',
|
||||
translationsStartIndex
|
||||
);
|
||||
if (translationsBracketIndex !== -1) {
|
||||
// Ищем закрывающую фигурную скобку с учетом вложенности
|
||||
let bracketCount = 1;
|
||||
let translationsEndIndex = translationsBracketIndex + 1;
|
||||
|
||||
while (
|
||||
translationsEndIndex < existingContent.length &&
|
||||
bracketCount > 0
|
||||
) {
|
||||
if (existingContent[translationsEndIndex] === '{') {
|
||||
bracketCount++;
|
||||
} else if (existingContent[translationsEndIndex] === '}') {
|
||||
bracketCount--;
|
||||
}
|
||||
translationsEndIndex++;
|
||||
}
|
||||
|
||||
if (bracketCount === 0) {
|
||||
const newTranslations = `${translationsStartPattern} {
|
||||
ru: ${JSON.stringify(template.translation, null, 4).replace(/^/gm, ' ')},
|
||||
}`;
|
||||
|
||||
existingContent =
|
||||
existingContent.substring(0, translationsStartIndex) +
|
||||
newTranslations +
|
||||
existingContent.substring(translationsEndIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем exampleData
|
||||
const exampleDataStartPattern = 'export const exampleData =';
|
||||
const exampleDataStartIndex = existingContent.indexOf(
|
||||
exampleDataStartPattern
|
||||
);
|
||||
|
||||
const newExampleData = `${exampleDataStartPattern} ${JSON.stringify(
|
||||
template.exampleData,
|
||||
null,
|
||||
2
|
||||
)}`;
|
||||
|
||||
if (exampleDataStartIndex !== -1) {
|
||||
const exampleDataBracketIndex = existingContent.indexOf(
|
||||
'{',
|
||||
exampleDataStartIndex
|
||||
);
|
||||
if (exampleDataBracketIndex !== -1) {
|
||||
// Ищем закрывающую фигурную скобку с учетом вложенности
|
||||
let bracketCount = 1;
|
||||
let exampleDataEndIndex = exampleDataBracketIndex + 1;
|
||||
|
||||
while (
|
||||
exampleDataEndIndex < existingContent.length &&
|
||||
bracketCount > 0
|
||||
) {
|
||||
if (existingContent[exampleDataEndIndex] === '{') {
|
||||
bracketCount++;
|
||||
} else if (existingContent[exampleDataEndIndex] === '}') {
|
||||
bracketCount--;
|
||||
}
|
||||
exampleDataEndIndex++;
|
||||
}
|
||||
|
||||
if (bracketCount === 0) {
|
||||
existingContent =
|
||||
existingContent.substring(0, exampleDataStartIndex) +
|
||||
newExampleData +
|
||||
existingContent.substring(exampleDataEndIndex);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Добавляем exampleData в конец файла
|
||||
existingContent += `\n\n// Пример данных для редактора шаблонов\n${newExampleData}\n`;
|
||||
}
|
||||
|
||||
// Сохраняем обновленный файл
|
||||
await window.electronAPI.writeCooptypeFile(template.id, existingContent);
|
||||
console.log('Файл шаблона успешно обновлен');
|
||||
} catch (error) {
|
||||
console.error('Ошибка при обновлении файла шаблона:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private getHardcodedTemplatesList(): TemplateFile[] {
|
||||
const templates = [
|
||||
'1.WalletAgreement',
|
||||
'100.ParticipantApplication',
|
||||
'1000.InvestmentAgreement',
|
||||
'1001.InvestByResultStatement',
|
||||
'1002.InvestByResultAct',
|
||||
'1005.InvestByMoneyStatement',
|
||||
'101.SelectBranchStatement',
|
||||
'1010.InvestMembershipConvertation',
|
||||
'2.RegulationElectronicSignature',
|
||||
'3.PrivacyPolicy',
|
||||
'300.AnnualGeneralMeetingAgenda',
|
||||
'301.AnnualGeneralMeetingSovietDecision',
|
||||
'302.AnnualGeneralMeetingNotification',
|
||||
'303.AnnualGeneralMeetingVotingBallot',
|
||||
'304.AnnualGeneralMeetingDecision',
|
||||
'4.UserAgreement',
|
||||
'50.CoopenomicsAgreement',
|
||||
'501.DecisionOfParticipantApplication',
|
||||
'599.ProjectFreeDecision',
|
||||
'600.FreeDecision',
|
||||
'700.AssetContributionStatement',
|
||||
'701.AssetContributionDecision',
|
||||
'702.AssetContributionAct',
|
||||
'800.ReturnByAssetStatement',
|
||||
'801.ReturnByAssetDecision',
|
||||
'802.ReturnByAssetAct',
|
||||
];
|
||||
|
||||
return templates.map((templateId) => {
|
||||
const name = this.formatTemplateName(templateId);
|
||||
const fileName = `${templateId}.json`;
|
||||
|
||||
return {
|
||||
id: templateId,
|
||||
name,
|
||||
title: name,
|
||||
fileName,
|
||||
@@ -68,64 +506,13 @@ export class TemplateService {
|
||||
});
|
||||
}
|
||||
|
||||
async loadTemplate(fileName: string): Promise<TemplateFile | null> {
|
||||
try {
|
||||
// В реальном приложении здесь будет запрос к серверу
|
||||
// Пока что возвращаем заглушку
|
||||
const response = await fetch(`/api/templates/${fileName}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Template not found');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
id: fileName.replace('.json', ''),
|
||||
name: this.formatTemplateName(fileName),
|
||||
title: this.formatTemplateName(fileName),
|
||||
fileName,
|
||||
context: data.context || '',
|
||||
translation: data.translation || {},
|
||||
exampleData: data.object_model || data.model || {},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading template:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async saveTemplate(template: TemplateFile): Promise<boolean> {
|
||||
try {
|
||||
// В реальном приложении здесь будет запрос к серверу для сохранения
|
||||
const data = {
|
||||
context: template.context,
|
||||
translation: template.translation,
|
||||
exampleData: template.exampleData,
|
||||
};
|
||||
|
||||
const response = await fetch(`/api/templates/${template.fileName}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Error saving template:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private formatTemplateName(fileName: string): string {
|
||||
private formatTemplateName(templateId: string): string {
|
||||
// Форматируем имя файла для отображения
|
||||
const name = fileName.replace('.json', '');
|
||||
const parts = name.split('.');
|
||||
const parts = templateId.split('.');
|
||||
|
||||
if (parts.length === 2) {
|
||||
const [id, namepart] = parts;
|
||||
// Преобразуем camelCase в читаемый формат
|
||||
// Преобразуем PascalCase в читаемый формат
|
||||
const formatted = namepart
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, (str) => str.toUpperCase())
|
||||
@@ -134,6 +521,6 @@ export class TemplateService {
|
||||
return `${id}. ${formatted}`;
|
||||
}
|
||||
|
||||
return name;
|
||||
return templateId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useSettingsStore = defineStore('settings', {
|
||||
state: () => ({
|
||||
registryPath: '',
|
||||
isFirstLaunch: true,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setRegistryPath(path: string) {
|
||||
this.registryPath = path;
|
||||
this.isFirstLaunch = false;
|
||||
},
|
||||
|
||||
resetFirstLaunch() {
|
||||
this.isFirstLaunch = false;
|
||||
},
|
||||
},
|
||||
|
||||
persist: {
|
||||
enabled: true,
|
||||
strategies: [
|
||||
{
|
||||
key: 'app-settings',
|
||||
storage: localStorage,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
+74
-7
@@ -25,12 +25,30 @@ export const useAppState = defineStore('app', {
|
||||
}),
|
||||
actions: {
|
||||
setContext(context: string, model: any) {
|
||||
console.log('🔧 setContext called with:', {
|
||||
contextLength: context?.length || 0,
|
||||
modelVarsKeys: Object.keys(model.vars || {}),
|
||||
modelTranslationKeys: Object.keys(model.translation || {}),
|
||||
modelObjectModelKeys: Object.keys(model.object_model || {}),
|
||||
modelObjectModelSample:
|
||||
JSON.stringify(model.object_model || {}).substring(0, 200) + '...',
|
||||
});
|
||||
|
||||
this.context = context;
|
||||
this.model = {
|
||||
vars: model.vars || {},
|
||||
translation: model.translation || {},
|
||||
object_model: model.object_model || {},
|
||||
};
|
||||
|
||||
console.log('✅ Context and model set:', {
|
||||
contextLength: this.context.length,
|
||||
modelKeys: {
|
||||
vars: Object.keys(this.model.vars),
|
||||
translation: Object.keys(this.model.translation),
|
||||
object_model: Object.keys(this.model.object_model),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// Новые actions для работы с множественными шаблонами
|
||||
@@ -40,12 +58,35 @@ export const useAppState = defineStore('app', {
|
||||
},
|
||||
|
||||
setCurrentTemplate(template: TemplateFile) {
|
||||
console.log('🎯 Setting current template:', {
|
||||
templateId: template.id,
|
||||
templateName: template.name,
|
||||
hasExampleData: !!(
|
||||
template.exampleData && Object.keys(template.exampleData).length > 0
|
||||
),
|
||||
exampleDataKeys: Object.keys(template.exampleData || {}),
|
||||
exampleDataSample:
|
||||
JSON.stringify(template.exampleData || {}).substring(0, 200) + '...',
|
||||
contextLength: template.context?.length || 0,
|
||||
});
|
||||
|
||||
this.currentTemplate = template;
|
||||
this.setContext(template.context, {
|
||||
// Полностью перезаписываем данные новым шаблоном, не сохраняя старые
|
||||
const modelData = {
|
||||
vars: {},
|
||||
translation: template.translation,
|
||||
object_model: template.exampleData,
|
||||
object_model: template.exampleData || {}, // Если нет exampleData, используем пустой объект
|
||||
};
|
||||
|
||||
console.log('📝 Setting context with model:', {
|
||||
contextLength: template.context?.length || 0,
|
||||
translationKeys: Object.keys(modelData.translation || {}),
|
||||
objectModelKeys: Object.keys(modelData.object_model || {}),
|
||||
objectModelSample:
|
||||
JSON.stringify(modelData.object_model).substring(0, 200) + '...',
|
||||
});
|
||||
|
||||
this.setContext(template.context, modelData);
|
||||
},
|
||||
|
||||
updateCurrentTemplate() {
|
||||
@@ -75,6 +116,16 @@ export const useAppState = defineStore('app', {
|
||||
},
|
||||
|
||||
parseContext() {
|
||||
console.log('🔍 parseContext started with current state:', {
|
||||
currentTemplateId: this.currentTemplate?.id || 'none',
|
||||
currentTemplateHasExampleData: !!(
|
||||
this.currentTemplate?.exampleData &&
|
||||
Object.keys(this.currentTemplate.exampleData).length > 0
|
||||
),
|
||||
currentModelObjectModelKeys: Object.keys(this.model.object_model || {}),
|
||||
contextLength: this.context?.length || 0,
|
||||
});
|
||||
|
||||
const variableRegex = /{{\s*([\w\.]+)(?:\s*\|\s*\w+)?\s*}}/g;
|
||||
// Обновлённый translationRegex для работы с переменными без именования
|
||||
const translationRegex = /{% trans '([^']+)'(?:,\s*([\w\.]+))* %}/g;
|
||||
@@ -194,7 +245,7 @@ export const useAppState = defineStore('app', {
|
||||
});
|
||||
}
|
||||
|
||||
// Мержим существующие данные
|
||||
// Мержим существующие данные только для текущего шаблона
|
||||
const mergeWithPreservation = (
|
||||
target: Record<string, any>,
|
||||
source: Record<string, any>
|
||||
@@ -223,13 +274,29 @@ export const useAppState = defineStore('app', {
|
||||
mergeWithPreservation(this.model.vars, variables);
|
||||
mergeWithPreservation(this.model.translation, translations);
|
||||
|
||||
// Объединяем object_model
|
||||
mergeWithPreservation(this.model.object_model, objectModel);
|
||||
// Если есть exampleData в текущем шаблоне, используем его как базу
|
||||
if (
|
||||
this.currentTemplate &&
|
||||
this.currentTemplate.exampleData &&
|
||||
Object.keys(this.currentTemplate.exampleData).length > 0
|
||||
) {
|
||||
// Мержим с exampleData как базой
|
||||
const baseObjectModel = { ...this.currentTemplate.exampleData };
|
||||
mergeWithPreservation(baseObjectModel, objectModel);
|
||||
this.model.object_model = baseObjectModel;
|
||||
} else {
|
||||
// Если нет exampleData, используем только парсинг из контекста
|
||||
this.model.object_model = objectModel;
|
||||
}
|
||||
|
||||
console.log('После парсинга (исключены переменные циклов):', {
|
||||
console.log('После парсинга:', {
|
||||
loopVariables: Array.from(loopVariables),
|
||||
vars: this.model.vars,
|
||||
object_model: this.model.object_model,
|
||||
hasExampleData: !!(
|
||||
this.currentTemplate &&
|
||||
Object.keys(this.currentTemplate.exampleData || {}).length > 0
|
||||
),
|
||||
});
|
||||
} else {
|
||||
this.model = {
|
||||
@@ -262,7 +329,7 @@ export const useAppState = defineStore('app', {
|
||||
{
|
||||
key: 'app',
|
||||
storage: localStorage,
|
||||
paths: ['context', 'model', 'html', 'currentTemplate'],
|
||||
paths: ['context', 'html', 'currentTemplate'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
{
|
||||
"context": "<div class=\"digital-document\"><p style=\"text-align: center;\"><h3>{% trans 'AGENDA_PROPOSAL_TITLE' %}</h3></p><p style=\"text-align: center;\">{% if meet.type == 'regular' %}{% trans 'ANNUAL_REGULAR' %}{% else %}{% trans 'ANNUAL_EXTRAORDINARY' %}{% endif %} {% trans 'GENERAL_MEETING_SHAREHOLDERS' %}</p><p style=\"text-align: center;\">{{vars.full_abbr_genitive}} «{{vars.name}}»</p><p style=\"text-align: right;\">{{ coop.city }}, {{ meet.created_at_day }} {{ meet.created_at_month }} {{ meet.created_at_year }} г.</p><p>{% trans 'MEETING_DATE_LABEL' %}: {{ meet.open_at_date }}</p><p>{% trans 'MEETING_TIME_LABEL' %}: {{ meet.open_at_time }}</p><p>{% trans 'MEETING_FORMAT_LABEL' %}: {% trans 'MEETING_FORMAT_VALUE' %}</p><p>{% trans 'REGISTRATION_DATETIME' %}: {{ meet.registration_datetime }}</p><p>{% trans 'VOTING_DEADLINE' %}: {% trans 'NO_LATER_THAN' %} {{ meet.close_at_datetime }}</p><h3>{% trans 'AGENDA_QUESTIONS' %}:</h3>{% for question in questions %}<p>{{ question.number }}. {{ question.title }}</p>{% if question.context %}<p>{{ question.context }}</p>{% endif %}{% endfor %}<h3>{% trans 'PROJECT_DECISIONS' %}:</h3>{% for question in questions %}<p>{% trans 'PROJECT_DECISION_LABEL' %} {{ question.number }}: {{ question.decision }}</p>{% endfor %}<p>{{ meet.presider_last_name }} {{ meet.presider_first_name }} {{ meet.presider_middle_name }}</p></div>\n\n<style>\n .digital-document {\n padding: 20px;\n white-space: pre-wrap;\n }\n</style>",
|
||||
"model": {
|
||||
"coop.city": "Москва",
|
||||
"meet.type": "regular",
|
||||
"meet.created_at_day": "12",
|
||||
"meet.created_at_month": "февраля",
|
||||
"meet.created_at_year": "2024",
|
||||
"meet.open_at_date": "12.03.2024",
|
||||
"meet.open_at_time": "10:00",
|
||||
"meet.registration_datetime": "12.03.2024, 09:30",
|
||||
"meet.close_at_datetime": "15.03.2024",
|
||||
"meet.presider_last_name": "Иванов",
|
||||
"meet.presider_first_name": "Петр",
|
||||
"meet.presider_middle_name": "Сидорович",
|
||||
"question.number": "1",
|
||||
"question.title": "Отчет Председателя Потребительского Кооператива о проделанной работе за период с 01.01.2023 г. по 31.12.2023 г.",
|
||||
"question.context": "Подробная информация об отчете приведена в документе...",
|
||||
"question.decision": "Принять Отчет Председателя Совета и признать результаты деятельности удовлетворительными",
|
||||
"vars.full_abbr_genitive": "Потребительского Кооператива",
|
||||
"vars.name": "ВОСХОД"
|
||||
},
|
||||
"translation": {
|
||||
"AGENDA_PROPOSAL_TITLE": "ПРЕДЛОЖЕНИЕ ПОВЕСТКИ",
|
||||
"ANNUAL_REGULAR": "ОЧЕРЕДНОГО",
|
||||
"ANNUAL_EXTRAORDINARY": "ВНЕОЧЕРЕДНОГО",
|
||||
"GENERAL_MEETING_SHAREHOLDERS": "СОБРАНИЯ ПАЙЩИКОВ",
|
||||
"MEETING_DATE_LABEL": "Дата проведения Собрания",
|
||||
"MEETING_TIME_LABEL": "Время проведения Собрания",
|
||||
"MEETING_FORMAT_LABEL": "Форма проведения собрания",
|
||||
"MEETING_FORMAT_VALUE": "заочное",
|
||||
"REGISTRATION_DATETIME": "Дата и время регистрации участников Собрания",
|
||||
"VOTING_DEADLINE": "Дата и время сбора заявлений пайщиков с результатом голосования",
|
||||
"NO_LATER_THAN": "не позднее",
|
||||
"AGENDA_QUESTIONS": "ВОПРОСЫ ПОВЕСТКИ ДНЯ",
|
||||
"PROJECT_DECISIONS": "ПРОЕКТЫ РЕШЕНИЙ",
|
||||
"PROJECT_DECISION_LABEL": "ПРОЕКТ РЕШЕНИЯ по вопросу"
|
||||
},
|
||||
"object_model": {
|
||||
"coop": {
|
||||
"city": "Москва"
|
||||
},
|
||||
"meet": {
|
||||
"type": "regular",
|
||||
"created_at_day": "12",
|
||||
"created_at_month": "февраля",
|
||||
"created_at_year": "2024",
|
||||
"open_at_date": "12.03.2024",
|
||||
"open_at_time": "10:00",
|
||||
"registration_datetime": "12.03.2024, 09:30",
|
||||
"close_at_datetime": "15.03.2024",
|
||||
"presider_last_name": "Иванов",
|
||||
"presider_first_name": "Петр",
|
||||
"presider_middle_name": "Сидорович"
|
||||
},
|
||||
"questions": [
|
||||
{
|
||||
"number": "1",
|
||||
"title": "Отчет Председателя Потребительского Кооператива о проделанной работе за период с 01.01.2023 г. по 31.12.2023 г.",
|
||||
"context": "Подробная информация об отчете приведена в документе...",
|
||||
"decision": "Принять Отчет Председателя Совета и признать результаты деятельности удовлетворительными"
|
||||
},
|
||||
{
|
||||
"number": "2",
|
||||
"title": "Утверждение Заключения Ревизионной Комиссии о ревизии деятельности за период с 01.01.2023 г. по 31.12.2023 г.",
|
||||
"context": "Заключение Ревизионной Комиссии содержит...",
|
||||
"decision": "Утвердить Заключение Ревизионной Комиссии о ревизии деятельности"
|
||||
}
|
||||
],
|
||||
"vars": {
|
||||
"full_abbr_genitive": "Потребительского Кооператива",
|
||||
"name": "ВОСХОД"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user