fix: 统一端口并完善桌面端授权和安装
This commit is contained in:
@@ -28,7 +28,7 @@ CapacityReport 用于导入每周容量报表数据,按 `Configure.json` 的
|
||||
CapaReport/
|
||||
├─ app/ # FastAPI 后端源码
|
||||
│ ├─ api/routers/ # API 路由
|
||||
│ ├─ services/ # 运行时和远程下载服务
|
||||
│ ├─ services/ # 授权、远程下载等业务服务
|
||||
│ ├─ utils/ # 通用工具
|
||||
│ ├─ main.py # 后端入口和前端托管
|
||||
│ ├─ processor.py # 数据处理主流程
|
||||
@@ -63,12 +63,11 @@ uv venv
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
安装并构建前端:
|
||||
安装前端依赖:
|
||||
|
||||
```powershell
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
cd ..
|
||||
```
|
||||
|
||||
@@ -91,7 +90,7 @@ cd frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Vite 会把 `/api` 和 `/health` 代理到 `http://localhost:9081`。
|
||||
Vite 会把 `/api` 和 `/health` 代理到 `http://localhost:9081`。如果 `frontend/dist` 不存在,`run.bat` 会自动安装前端依赖并执行构建。
|
||||
|
||||
## 一键构建
|
||||
|
||||
@@ -148,10 +147,10 @@ scripts\build.bat desktop
|
||||
dist\desktop\*-setup.exe
|
||||
```
|
||||
|
||||
桌面版使用 Tauri 启动 Python sidecar。sidecar 默认监听 `127.0.0.1:19082`,运行数据写入系统 app data 目录,不写入安装目录。
|
||||
桌面版使用 Tauri 启动 Python sidecar。sidecar 默认监听 `127.0.0.1:9081`,运行数据写入系统 app data 目录,不写入安装目录。
|
||||
首次启动会把安装包内置的 `Configure.json` 和 `ReportScript.sql` 复制到运行数据目录;Windows 下通常是 `%APPDATA%\com.nixevol.capacityreport\`。安装目录中的 `_up_` 只是 Tauri 打包资源目录,程序运行时不会直接编辑它。
|
||||
Windows 桌面版使用 NSIS 安装器,卸载时会询问是否同时删除 `%APPDATA%\com.nixevol.capacityreport\` 中的配置、脚本、授权、缓存和日志。
|
||||
桌面版 release 包已启用 DevTools,如需排查前端请求,可在窗口中按 `F12` 或右键打开开发者工具。
|
||||
Windows 桌面版默认安装到 `D:\Program Files\CapacityReport`;如果没有 D 盘,则默认安装到系统 `Program Files\CapacityReport`。桌面版已关闭 release DevTools 和右键浏览器菜单。
|
||||
|
||||
构建桌面版需要 Rust 和 Tauri CLI。脚本会在缺少 Tauri CLI 时自动执行:
|
||||
|
||||
@@ -185,7 +184,7 @@ docker compose -f dist\docker\docker-compose.yml up -d
|
||||
访问:
|
||||
|
||||
```text
|
||||
http://localhost:19081
|
||||
http://localhost:9081
|
||||
```
|
||||
|
||||
停止:
|
||||
@@ -219,7 +218,7 @@ Server Portable 和桌面版需要在目标系统原生构建:Windows 包在 W
|
||||
|
||||
登录密码保存在本地 `auth.ini`,该文件不应提交到版本库。
|
||||
|
||||
授权到期日期保存在本地加密文件 `license.dat`,默认到期日为 `2026-06-20`。处理任务不会读取系统日期,而是从任务目录 ZIP 文件名中的 `YYYYMMDDHHMM` 或 `YYYYMMDDHHMMSS` 时间戳取最大日期进行比对。
|
||||
授权到期日期保存在本地加密文件 `license.dat`,默认到期日由 `app/services/license.py` 中的 `DEFAULT_EXPIRES_ON` 控制,当前为 `2026-06-20`。处理任务不会读取系统日期,而是从任务目录 ZIP 文件名中的 `YYYYMMDDHHMM` 或 `YYYYMMDDHHMMSS` 时间戳取最大日期进行比对。登录后连续点击左上角品牌图标 8 次,可主动打开授权延期窗口。
|
||||
|
||||
## 常用接口
|
||||
|
||||
@@ -234,7 +233,6 @@ Server Portable 和桌面版需要在目标系统原生构建:Windows 包在 W
|
||||
- `POST /api/license/activate`:提交激活码并顺延授权期限
|
||||
- `GET /api/history`:处理历史
|
||||
- `POST /api/history/download`:下载历史原始数据
|
||||
- `POST /api/service/restart`:重启服务
|
||||
- `GET /health`:健康检查
|
||||
|
||||
## 维护注意事项
|
||||
|
||||
@@ -32,7 +32,7 @@ async def health_check():
|
||||
return {
|
||||
"status": "healthy" if is_healthy else "unhealthy",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"version": "2.0.3",
|
||||
"version": "3.0.0",
|
||||
"uptime_pid": os.getpid(),
|
||||
"checks": checks,
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import os
|
||||
import platform
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.services.runtime import (
|
||||
is_container_runtime,
|
||||
is_supervisor_running,
|
||||
restart_method_name,
|
||||
restart_via_supervisor,
|
||||
terminate_current_process,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(tags=["service"])
|
||||
|
||||
|
||||
@router.post("/api/service/restart")
|
||||
async def restart_service():
|
||||
supervisor_running = is_supervisor_running()
|
||||
container_runtime = is_container_runtime()
|
||||
|
||||
if supervisor_running:
|
||||
success, message = restart_via_supervisor()
|
||||
if success:
|
||||
return {
|
||||
"success": True,
|
||||
"message": message,
|
||||
"method": "supervisor",
|
||||
"container": container_runtime,
|
||||
}
|
||||
|
||||
method = restart_method_name(supervisor_running)
|
||||
thread = Thread(target=_delayed_exit, daemon=True)
|
||||
thread.start()
|
||||
|
||||
message = "服务正在重启,请稍后刷新页面..."
|
||||
if supervisor_running:
|
||||
message = "supervisor 命令不可用,已切换为进程重启..."
|
||||
elif container_runtime:
|
||||
message = "服务进程正在退出,容器或进程管理器将拉起服务..."
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": message,
|
||||
"method": method,
|
||||
"container": container_runtime,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/api/service/status")
|
||||
async def get_service_status():
|
||||
return {
|
||||
"status": "running",
|
||||
"version": "2.0.3",
|
||||
"platform": platform.system(),
|
||||
"supervisor": is_supervisor_running(),
|
||||
"container": is_container_runtime(),
|
||||
"pid": os.getpid(),
|
||||
"python_version": platform.python_version(),
|
||||
}
|
||||
|
||||
|
||||
def _delayed_exit() -> None:
|
||||
time.sleep(1)
|
||||
terminate_current_process()
|
||||
+2
-3
@@ -7,12 +7,12 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
|
||||
from app import state
|
||||
from app.api.routers import auth, cache, config, database, health, history, license, remote, script, service, tasks, upload
|
||||
from app.api.routers import auth, cache, config, database, health, history, license, remote, script, tasks, upload
|
||||
from app.auth import verify_jwt_token
|
||||
from app.config import BASE_DIR
|
||||
|
||||
|
||||
APP_VERSION = "2.0.3"
|
||||
APP_VERSION = "3.0.0"
|
||||
APP_HOST = "0.0.0.0"
|
||||
APP_PORT = 9081
|
||||
FRONTEND_DIST_DIR = BASE_DIR / "frontend" / "dist"
|
||||
@@ -71,7 +71,6 @@ def register_routes(app: FastAPI) -> None:
|
||||
database.router,
|
||||
config.router,
|
||||
cache.router,
|
||||
service.router,
|
||||
script.router,
|
||||
]
|
||||
for router in routers:
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import os
|
||||
import platform
|
||||
import signal
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def is_supervisor_running() -> bool:
|
||||
if os.environ.get("SUPERVISOR_ENABLED") == "1":
|
||||
return True
|
||||
|
||||
supervisor_sock = Path("/var/run/supervisor.sock")
|
||||
if supervisor_sock.exists() and _command_succeeds(["supervisorctl", "status"], timeout=5):
|
||||
return True
|
||||
|
||||
try:
|
||||
import psutil # pyright: ignore[reportMissingModuleSource]
|
||||
|
||||
parent = psutil.Process().parent()
|
||||
if parent and "supervisor" in parent.name().lower():
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return _command_succeeds(["pgrep", "-f", "supervisord"], timeout=2)
|
||||
|
||||
|
||||
def is_container_runtime() -> bool:
|
||||
if os.environ.get("container"):
|
||||
return True
|
||||
if Path("/.dockerenv").exists():
|
||||
return True
|
||||
|
||||
cgroup_path = Path("/proc/1/cgroup")
|
||||
if not cgroup_path.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
cgroup = cgroup_path.read_text(encoding="utf-8", errors="ignore").lower()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return any(marker in cgroup for marker in ("docker", "kubepods", "containerd"))
|
||||
|
||||
|
||||
def restart_via_supervisor() -> tuple[bool, str]:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["supervisorctl", "restart", "fastapi"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True, "服务正在通过 supervisor 重启..."
|
||||
|
||||
error_msg = result.stderr or result.stdout or "未知错误"
|
||||
return False, f"重启失败: {error_msg}"
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "重启操作超时"
|
||||
except FileNotFoundError:
|
||||
return False, "找不到 supervisorctl 命令"
|
||||
except Exception as exc:
|
||||
return False, f"重启异常: {exc}"
|
||||
|
||||
|
||||
def terminate_current_process() -> None:
|
||||
if platform.system() == "Windows":
|
||||
os._exit(0)
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
|
||||
|
||||
def restart_method_name(supervisor_running: bool) -> str:
|
||||
if supervisor_running:
|
||||
return "supervisor"
|
||||
if platform.system() == "Windows":
|
||||
return "windows-process"
|
||||
if is_container_runtime():
|
||||
return "container-process"
|
||||
return "process"
|
||||
|
||||
|
||||
def _command_succeeds(command: list[str], timeout: int) -> bool:
|
||||
try:
|
||||
result = subprocess.run(command, capture_output=True, text=True, timeout=timeout)
|
||||
return result.returncode == 0 and bool(result.stdout.strip() or command[0] == "supervisorctl")
|
||||
except Exception:
|
||||
return False
|
||||
+11
-2
@@ -1,4 +1,15 @@
|
||||
# 项目上下文记录
|
||||
## 2026-05-20:统一端口、升级 3.0.0 并收敛桌面安装行为
|
||||
|
||||
- 应用户要求,应用访问端口统一回 `9081`:Server Portable、Docker 宿主机映射、Tauri 桌面 sidecar、桌面前端 `VITE_API_BASE` 和前端 Tauri 兜底 API 地址均使用 `http://127.0.0.1:9081`;桌面版启动前只会清理同名 `capareport-server.exe` 的残留监听进程,避免误杀其它占用 `9081` 的程序。
|
||||
- 版本统一提升为 `3.0.0`,同步更新后端 `APP_VERSION`、健康检查、侧边栏显示、Tauri 配置和 Cargo 包版本。
|
||||
- `run.bat` 不再因 `frontend/dist/index.html` 缺失直接退出;缺少前端构建产物时会检查 `npm`、按需执行 `npm ci`,然后自动运行 `npm run build` 再启动后端。
|
||||
- 桌面版去除 release DevTools:`src-tauri/Cargo.toml` 移除 Tauri `devtools` feature,`tauri.conf.json` 移除窗口 `devtools` 配置,前端在 Tauri 环境下阻止右键浏览器菜单和 `F12`/`Ctrl+Shift+I`。
|
||||
- Windows NSIS 安装器改为 per-machine,并在未选择自定义安装目录时默认落到 `D:\Program Files\CapacityReport`;如果没有 D 盘,则使用系统 `Program Files\CapacityReport`。`scripts/build.ps1` 构建桌面版时会临时生成 Tauri NSIS 模板并恢复 `tauri.conf.json`,避免旧安装记录把默认路径带回 C 盘。默认授权到期日仍由 `app/services/license.py` 的 `DEFAULT_EXPIRES_ON` 控制。
|
||||
- 桌面端不再提供服务重启功能:前端移除重启按钮和等待遮罩,后端删除 `/api/service/restart`、`/api/service/status` 以及对应 runtime 重启实现,避免桌面 sidecar 无法可靠自重启时误导用户。
|
||||
- 登录后连续点击左上角品牌图标 8 次会主动打开授权延期窗口,窗口显示当前激活 key 标签并允许连续提交激活码,每次成功后按新的到期日刷新下一次 key。
|
||||
- 已验证 `cmd /c scripts\build.bat desktop` 可生成 `dist\desktop\CapacityReport_3.0.0_x64-setup.exe`;静默安装后 `capacity-report-desktop.exe`、`capareport-server.exe`、`Configure.json` 和 `ReportScript.sql` 均位于 `D:\Program Files\CapacityReport`,注册表 `InstallLocation` 指向 D 盘。已启动安装后的桌面程序验证 sidecar `/health` 返回 `3.0.0`,并验证配置、脚本和授权接口可读取;验证后已停止测试进程并清理中间产物。
|
||||
|
||||
## 2026-05-20:修复桌面版跨源预检导致配置网络错误
|
||||
|
||||
- 桌面版前端访问 `127.0.0.1:19082` 属于 WebView 跨源请求,带 `Authorization` 或上传配置文件时浏览器会先发 `OPTIONS` 预检;`app/main.py` 的 JWT 中间件现在直接放行 `OPTIONS`,让 FastAPI CORS 中间件返回允许头,避免配置读取和配置上传显示“网络错误”。
|
||||
@@ -381,12 +392,10 @@
|
||||
- `database.py`:数据库测试、表查询、表维护和导出。
|
||||
- `config.py`:配置读取、保存、上传和下载。
|
||||
- `cache.py`:缓存大小统计。
|
||||
- `service.py`:服务状态和重启。
|
||||
- `script.py`:SQL 脚本读取、保存和执行。
|
||||
- `health.py`:健康检查。
|
||||
- 运行时共享状态放在 `app/state.py`,包括配置实例、历史管理器、处理任务、上传会话和全局任务锁。
|
||||
- 登录、密码文件和 Token 逻辑放在 `app/auth.py`,继续使用本地 `auth.ini`。
|
||||
- 服务重启检测和进程退出逻辑放在 `app/services/runtime.py`。
|
||||
- 文件大小等工具函数放在 `app/utils/files.py`。
|
||||
|
||||
### 前端
|
||||
|
||||
+96
-84
@@ -11,7 +11,17 @@
|
||||
@update:collapsed="handleSidebarCollapsed"
|
||||
>
|
||||
<div class="brand">
|
||||
<div class="brand-mark">📊</div>
|
||||
<div
|
||||
class="brand-mark"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
title="CapacityReport"
|
||||
@click="handleBrandMarkClick"
|
||||
@keydown.enter.prevent="handleBrandMarkClick"
|
||||
@keydown.space.prevent="handleBrandMarkClick"
|
||||
>
|
||||
📊
|
||||
</div>
|
||||
<div class="brand-text">CapacityReport</div>
|
||||
</div>
|
||||
<n-menu
|
||||
@@ -25,7 +35,7 @@
|
||||
<div class="sider-footer">
|
||||
<span class="sider-version">
|
||||
<span class="sider-version-label">版本:</span>
|
||||
<span class="sider-version-number">v2.0.3</span>
|
||||
<span class="sider-version-number">v3.0.0</span>
|
||||
</span>
|
||||
<span class="sider-powered">Power by:NIXEVOL</span>
|
||||
</div>
|
||||
@@ -88,15 +98,6 @@
|
||||
<button class="theme-toggle" type="button" :title="themeToggleTitle" @click="toggleTheme">
|
||||
<n-icon><component :is="themeToggleIcon" /></n-icon>
|
||||
</button>
|
||||
<button
|
||||
class="restart-btn"
|
||||
type="button"
|
||||
title="重启服务"
|
||||
:disabled="restarting"
|
||||
@click="restartService"
|
||||
>
|
||||
<n-icon><PowerOutline /></n-icon>
|
||||
</button>
|
||||
<n-button tertiary circle title="退出登录" class="logout-button" @click="logout">
|
||||
<template #icon>
|
||||
<n-icon><LogOutOutline /></n-icon>
|
||||
@@ -111,18 +112,37 @@
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
|
||||
<div v-if="restarting" class="restart-overlay" role="status" aria-live="assertive">
|
||||
<div class="restart-overlay-content">
|
||||
<div class="restart-overlay-spinner"></div>
|
||||
<div class="restart-overlay-text">{{ restartOverlayText }}</div>
|
||||
<n-modal
|
||||
v-model:show="licenseModalVisible"
|
||||
preset="card"
|
||||
title="授权延期"
|
||||
:mask-closable="!activationLoading"
|
||||
:style="{ width: '420px', maxWidth: 'calc(100vw - 32px)' }"
|
||||
>
|
||||
<div class="license-dialog-body">
|
||||
<p class="license-dialog-text">{{ licenseMessage }}</p>
|
||||
<div class="license-key-label">key: {{ activationKeyLabel }}</div>
|
||||
<n-input
|
||||
v-model:value="activationCode"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="请输入激活码"
|
||||
:disabled="activationLoading"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="license-dialog-footer">
|
||||
<n-button :disabled="activationLoading" @click="licenseModalVisible = false">取消</n-button>
|
||||
<n-button type="primary" :loading="activationLoading" @click="submitActivation">激活</n-button>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onBeforeUnmount, onMounted, ref, watch, type Component } from 'vue';
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router';
|
||||
import { useDialog, useMessage, type DropdownOption, type MenuOption, NIcon } from 'naive-ui';
|
||||
import { useMessage, type DropdownOption, type MenuOption, NIcon } from 'naive-ui';
|
||||
import {
|
||||
ChevronDownOutline,
|
||||
CloudUploadOutline,
|
||||
@@ -130,30 +150,32 @@ import {
|
||||
FileTrayFullOutline,
|
||||
LogOutOutline,
|
||||
MoonOutline,
|
||||
PowerOutline,
|
||||
ServerOutline,
|
||||
SettingsOutline,
|
||||
SunnyOutline
|
||||
} from '@vicons/ionicons5';
|
||||
|
||||
import { apiGet, apiPost, clearToken, getToken, setToken, setUnauthorizedHandler } from './api/client';
|
||||
import type { ApiMessage, LoginResponse, ServiceStatus } from './types';
|
||||
import type { LicenseStatus, LoginResponse } from './types';
|
||||
import LoginView from './components/LoginView.vue';
|
||||
import { pageHeader, resetPageHeader, resolveHeaderValue, type PageHeaderAction } from './composables/pageHeader';
|
||||
import { themeName, toggleAppTheme } from './composables/theme';
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const token = ref(getToken());
|
||||
const loginLoading = ref(false);
|
||||
const restarting = ref(false);
|
||||
const restartOverlayText = ref('正在重启服务...');
|
||||
const sidebarCollapsed = ref(localStorage.getItem('sidebarCollapsed') === 'true');
|
||||
const licenseModalVisible = ref(false);
|
||||
const activationCode = ref('');
|
||||
const activationLoading = ref(false);
|
||||
const activationKeyLabel = ref('2026/06/20');
|
||||
const licenseMessage = ref('输入激活码可将授权到期日期延长 30 天。');
|
||||
const brandClickCount = ref(0);
|
||||
const menuKeys = ['workflow', 'history', 'database', 'script', 'settings'] as const;
|
||||
type MenuKey = (typeof menuKeys)[number];
|
||||
let restartPollTimer: number | undefined;
|
||||
let brandClickResetTimer: number | undefined;
|
||||
|
||||
const menuOptions: MenuOption[] = [
|
||||
{ label: '数据上传', key: 'workflow', icon: renderIcon(CloudUploadOutline) },
|
||||
@@ -182,8 +204,8 @@ onMounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('dragover', preventWindowFileDrop, { capture: true });
|
||||
window.removeEventListener('drop', preventWindowFileDrop, { capture: true });
|
||||
if (restartPollTimer !== undefined) {
|
||||
window.clearTimeout(restartPollTimer);
|
||||
if (brandClickResetTimer !== undefined) {
|
||||
window.clearTimeout(brandClickResetTimer);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -224,66 +246,6 @@ function toggleTheme() {
|
||||
toggleAppTheme();
|
||||
}
|
||||
|
||||
async function restartService() {
|
||||
if (restarting.value) return;
|
||||
|
||||
dialog.warning({
|
||||
title: '重启服务',
|
||||
content: '确定要重启服务吗?这将中断当前所有操作。',
|
||||
positiveText: '重启',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
void executeRestartService();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function executeRestartService() {
|
||||
restarting.value = true;
|
||||
restartOverlayText.value = '正在重启服务...';
|
||||
|
||||
try {
|
||||
const result = await apiPost<ApiMessage>('/api/service/restart');
|
||||
restartOverlayText.value = result.message || '正在等待服务恢复...';
|
||||
} catch {
|
||||
restartOverlayText.value = '正在等待服务恢复...';
|
||||
}
|
||||
|
||||
pollServiceStatus();
|
||||
}
|
||||
|
||||
function pollServiceStatus() {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 60;
|
||||
const pollInterval = 5000;
|
||||
|
||||
const checkService = async () => {
|
||||
attempts += 1;
|
||||
|
||||
try {
|
||||
await apiGet<ServiceStatus>('/api/service/status');
|
||||
restartOverlayText.value = '服务已恢复,正在刷新页面...';
|
||||
restartPollTimer = window.setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
return;
|
||||
} catch {
|
||||
restartOverlayText.value = `正在等待服务恢复... (${attempts}/${maxAttempts})`;
|
||||
}
|
||||
|
||||
if (attempts < maxAttempts) {
|
||||
restartPollTimer = window.setTimeout(checkService, pollInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
restarting.value = false;
|
||||
restartOverlayText.value = '正在重启服务...';
|
||||
message.warning('服务重启超时,请手动刷新页面');
|
||||
};
|
||||
|
||||
restartPollTimer = window.setTimeout(checkService, 3000);
|
||||
}
|
||||
|
||||
function handleMenuChange(key: string | number) {
|
||||
if (!isMenuKey(key) || key === activeMenu.value) {
|
||||
return;
|
||||
@@ -291,6 +253,56 @@ function handleMenuChange(key: string | number) {
|
||||
void router.push({ name: key });
|
||||
}
|
||||
|
||||
function handleBrandMarkClick() {
|
||||
brandClickCount.value += 1;
|
||||
if (brandClickResetTimer !== undefined) {
|
||||
window.clearTimeout(brandClickResetTimer);
|
||||
}
|
||||
|
||||
if (brandClickCount.value >= 8) {
|
||||
brandClickCount.value = 0;
|
||||
void openLicenseModal();
|
||||
return;
|
||||
}
|
||||
|
||||
brandClickResetTimer = window.setTimeout(() => {
|
||||
brandClickCount.value = 0;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
async function openLicenseModal() {
|
||||
try {
|
||||
const status = await apiGet<LicenseStatus>('/api/license/status');
|
||||
activationKeyLabel.value = status.key_label;
|
||||
licenseMessage.value = `当前授权到期日期:${status.expires_on}。输入激活码可延长 30 天。`;
|
||||
activationCode.value = '';
|
||||
licenseModalVisible.value = true;
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '获取授权状态失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitActivation() {
|
||||
const code = activationCode.value.trim();
|
||||
if (!code) {
|
||||
message.warning('请输入激活码');
|
||||
return;
|
||||
}
|
||||
|
||||
activationLoading.value = true;
|
||||
try {
|
||||
const result = await apiPost<LicenseStatus>('/api/license/activate', { code });
|
||||
activationKeyLabel.value = result.key_label;
|
||||
licenseMessage.value = `当前授权到期日期:${result.expires_on}。输入激活码可继续延长 30 天。`;
|
||||
activationCode.value = '';
|
||||
message.success(`激活成功,到期日期: ${result.expires_on}`);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '激活失败');
|
||||
} finally {
|
||||
activationLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function isMenuKey(value: unknown): value is MenuKey {
|
||||
return typeof value === 'string' && menuKeys.includes(value as MenuKey);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ApiError, ApiErrorDetail } from '../types';
|
||||
|
||||
const TOKEN_KEY = 'capacity_report_token';
|
||||
const DESKTOP_API_BASE = 'http://127.0.0.1:19082';
|
||||
const DESKTOP_API_BASE = 'http://127.0.0.1:9081';
|
||||
const API_BASE = resolveApiBase();
|
||||
const API_FETCH_RETRIES = 8;
|
||||
const API_FETCH_RETRY_DELAY_MS = 500;
|
||||
|
||||
@@ -5,4 +5,21 @@ import App from './App.vue';
|
||||
import { router } from './router';
|
||||
import './styles.css';
|
||||
|
||||
if (isTauriRuntime()) {
|
||||
window.addEventListener('contextmenu', event => event.preventDefault());
|
||||
window.addEventListener('keydown', event => {
|
||||
if (event.key === 'F12' || (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'i')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createApp(App).use(router).use(naive).mount('#app');
|
||||
|
||||
function isTauriRuntime(): boolean {
|
||||
const win = window as Window & {
|
||||
__TAURI__?: unknown;
|
||||
__TAURI_INTERNALS__?: unknown;
|
||||
};
|
||||
return Boolean(win.__TAURI__ || win.__TAURI_INTERNALS__ || navigator.userAgent.includes('Tauri'));
|
||||
}
|
||||
|
||||
+4
-71
@@ -182,6 +182,8 @@ select {
|
||||
font-size: 18px;
|
||||
background: linear-gradient(135deg, var(--td-brand-color), #618dff);
|
||||
border-radius: var(--td-radius-default);
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
@@ -352,8 +354,7 @@ select {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.theme-toggle,
|
||||
.restart-btn {
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -376,80 +377,12 @@ select {
|
||||
border-color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
.restart-btn {
|
||||
color: var(--td-error-color);
|
||||
border-color: var(--td-error-color);
|
||||
}
|
||||
|
||||
.restart-btn:hover {
|
||||
color: var(--td-error-color);
|
||||
background: var(--td-error-color-light);
|
||||
}
|
||||
|
||||
.restart-btn:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.theme-toggle svg,
|
||||
.restart-btn svg {
|
||||
.theme-toggle svg {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.restart-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 5000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--td-mask-color);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.restart-overlay-content {
|
||||
display: flex;
|
||||
min-width: 260px;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
padding: 32px 44px;
|
||||
color: var(--td-text-color-primary);
|
||||
background: var(--td-bg-color-container);
|
||||
border: 1px solid var(--td-border-color-light);
|
||||
border-radius: var(--td-radius-large);
|
||||
box-shadow: var(--td-shadow-3);
|
||||
}
|
||||
|
||||
.restart-overlay-spinner {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: 4px solid var(--td-border-color-light);
|
||||
border-top-color: var(--td-brand-color);
|
||||
border-radius: 50%;
|
||||
animation: spinner-rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
.restart-overlay-text {
|
||||
color: var(--td-text-color-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes spinner-rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -139,16 +139,6 @@ export interface DatabaseInfo {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ServiceStatus {
|
||||
status: string;
|
||||
version: string;
|
||||
platform: string;
|
||||
supervisor: boolean;
|
||||
container: boolean;
|
||||
pid: number;
|
||||
python_version: string;
|
||||
}
|
||||
|
||||
export interface ApiMessage {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
container_name: capacity-report-app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "19081:9081"
|
||||
- "9081:9081"
|
||||
volumes:
|
||||
- ./cache:/app/cache
|
||||
- ./Configure.json:/app/Configure.json
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@echo off
|
||||
title CapacityReport v2.0.2
|
||||
title CapacityReport v3.0.0
|
||||
|
||||
echo ========================================
|
||||
echo CapacityReport - report service
|
||||
@@ -28,13 +28,34 @@ if errorlevel 1 (
|
||||
)
|
||||
|
||||
if not exist "frontend\dist\index.html" (
|
||||
echo [ERROR] Frontend build output not found.
|
||||
echo [HINT] Run these commands first:
|
||||
echo cd frontend
|
||||
echo npm install
|
||||
echo npm run build
|
||||
echo [INFO] Frontend build output not found. Building frontend...
|
||||
where npm >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] npm not found. Install Node.js first.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
pushd frontend
|
||||
if not exist "node_modules" (
|
||||
echo [INFO] Installing frontend dependencies...
|
||||
call npm ci
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] Failed to install frontend dependencies.
|
||||
popd
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
call npm run build
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] Failed to build frontend.
|
||||
popd
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
popd
|
||||
)
|
||||
|
||||
:loop
|
||||
|
||||
+132
-1
@@ -27,7 +27,7 @@ $PyInstallerDir = Join-Path $TempDir "pyinstaller"
|
||||
$FrontendDir = Join-Path $Root "frontend"
|
||||
$PackagingDir = Join-Path $Root "packaging"
|
||||
$ServerName = "capareport-server"
|
||||
$DesktopApiBase = "http://127.0.0.1:19082"
|
||||
$DesktopApiBase = "http://127.0.0.1:9081"
|
||||
|
||||
function Write-Step($Message) {
|
||||
Write-Host ""
|
||||
@@ -209,6 +209,134 @@ function Write-TextFile($Path, $Content, [switch]$Lf) {
|
||||
}
|
||||
}
|
||||
|
||||
function Convert-ToNsisPath($Path) {
|
||||
return (Resolve-Path -LiteralPath $Path).Path
|
||||
}
|
||||
|
||||
function Get-TauriNsisTemplatePath {
|
||||
$registryDir = Join-Path $env:USERPROFILE ".cargo\registry\src"
|
||||
if (!(Test-Path $registryDir)) {
|
||||
throw "Cargo registry not found: $registryDir"
|
||||
}
|
||||
|
||||
$templates = Get-ChildItem -Path $registryDir -Recurse -Filter "installer.nsi" |
|
||||
Where-Object { $_.FullName -match "tauri-bundler-[^\\]+\\src\\bundle\\windows\\nsis\\installer\.nsi$" } |
|
||||
Sort-Object FullName -Descending
|
||||
|
||||
if (!$templates) {
|
||||
throw "Tauri NSIS template not found. Run cargo tauri build once to fetch tauri-bundler."
|
||||
}
|
||||
|
||||
return $templates[0].FullName
|
||||
}
|
||||
|
||||
function New-DesktopNsisTemplate {
|
||||
$source = Get-TauriNsisTemplatePath
|
||||
$target = Join-Path $TempDir "tauri-nsis-installer.nsi"
|
||||
$hookPath = Convert-ToNsisPath (Join-Path $Root "src-tauri\windows\nsis-hooks.nsh")
|
||||
$content = [System.IO.File]::ReadAllText($source)
|
||||
|
||||
$content = [regex]::Replace(
|
||||
$content,
|
||||
'\{\{#if installer_hooks\}\}\s*!include "\{\{installer_hooks\}\}"\s*\{\{/if\}\}',
|
||||
"!include `"$hookPath`""
|
||||
)
|
||||
|
||||
$restoreLine = " Call RestorePreviousInstallLocation"
|
||||
$installDirOverride = @'
|
||||
Call RestorePreviousInstallLocation
|
||||
|
||||
!if "${INSTALLMODE}" == "perMachine"
|
||||
${If} ${FileExists} "D:\*.*"
|
||||
StrCpy $INSTDIR "D:\Program Files\${PRODUCTNAME}"
|
||||
${Else}
|
||||
${If} ${RunningX64}
|
||||
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
|
||||
${Else}
|
||||
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
!endif
|
||||
'@
|
||||
|
||||
if (!$content.Contains($restoreLine)) {
|
||||
throw "Tauri NSIS template changed: RestorePreviousInstallLocation marker not found"
|
||||
}
|
||||
$content = $content.Replace($restoreLine, $installDirOverride)
|
||||
|
||||
$onInitTail = @'
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
!insertmacro MULTIUSER_INIT
|
||||
!endif
|
||||
FunctionEnd
|
||||
'@
|
||||
$onInitOverride = @'
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
!insertmacro MULTIUSER_INIT
|
||||
!endif
|
||||
|
||||
!if "${INSTALLMODE}" == "perMachine"
|
||||
${If} ${FileExists} "D:\*.*"
|
||||
StrCpy $INSTDIR "D:\Program Files\${PRODUCTNAME}"
|
||||
${Else}
|
||||
${If} ${RunningX64}
|
||||
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
|
||||
${Else}
|
||||
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
!endif
|
||||
FunctionEnd
|
||||
'@
|
||||
if (!$content.Contains($onInitTail)) {
|
||||
throw "Tauri NSIS template changed: .onInit tail marker not found"
|
||||
}
|
||||
$content = $content.Replace($onInitTail, $onInitOverride)
|
||||
|
||||
$restoreFunction = @'
|
||||
Function RestorePreviousInstallLocation
|
||||
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
|
||||
StrCmp $4 "" +2 0
|
||||
StrCpy $INSTDIR $4
|
||||
FunctionEnd
|
||||
'@
|
||||
$restoreOverride = @'
|
||||
Function RestorePreviousInstallLocation
|
||||
!if "${INSTALLMODE}" == "perMachine"
|
||||
${If} ${FileExists} "D:\*.*"
|
||||
StrCpy $INSTDIR "D:\Program Files\${PRODUCTNAME}"
|
||||
Return
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
|
||||
StrCmp $4 "" +2 0
|
||||
StrCpy $INSTDIR $4
|
||||
FunctionEnd
|
||||
'@
|
||||
if (!$content.Contains($restoreFunction)) {
|
||||
throw "Tauri NSIS template changed: RestorePreviousInstallLocation function marker not found"
|
||||
}
|
||||
$content = $content.Replace($restoreFunction, $restoreOverride)
|
||||
|
||||
Write-TextFile $target $content -Lf
|
||||
return $target
|
||||
}
|
||||
|
||||
function Invoke-WithDesktopTauriConfig($NsisTemplatePath, [scriptblock]$Action) {
|
||||
$configPath = Join-Path $Root "src-tauri\tauri.conf.json"
|
||||
$original = [System.IO.File]::ReadAllText($configPath)
|
||||
try {
|
||||
$config = $original | ConvertFrom-Json
|
||||
$config.bundle.windows.nsis | Add-Member -NotePropertyName "template" -NotePropertyValue $NsisTemplatePath -Force
|
||||
Write-TextFile $configPath ($config | ConvertTo-Json -Depth 50)
|
||||
& $Action
|
||||
}
|
||||
finally {
|
||||
[System.IO.File]::WriteAllText($configPath, $original, [System.Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-RuntimeFiles($OutputDir) {
|
||||
Copy-Item -Path (Join-Path $Root "Configure.json") -Destination (Join-Path $OutputDir "Configure.json") -Force
|
||||
Copy-Item -Path (Join-Path $Root "ReportScript.sql") -Destination (Join-Path $OutputDir "ReportScript.sql") -Force
|
||||
@@ -342,9 +470,12 @@ function Build-DesktopPackage {
|
||||
Invoke-Checked "cargo install tauri-cli --locked" $Root
|
||||
}
|
||||
|
||||
$nsisTemplate = New-DesktopNsisTemplate
|
||||
Invoke-WithDesktopTauriConfig $nsisTemplate {
|
||||
Remove-Tree (Join-Path $Root "src-tauri\target\release\bundle")
|
||||
Write-Step "Building Tauri desktop package"
|
||||
Invoke-Checked "cargo tauri build" (Join-Path $Root "src-tauri")
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $DesktopDistDir | Out-Null
|
||||
$bundleDir = Join-Path $Root "src-tauri\target\release\bundle"
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ DOCKER_DIST_DIR="$DIST_DIR/docker"
|
||||
TMP_DIR="$DIST_DIR/.tmp"
|
||||
PYINSTALLER_DIR="$TMP_DIR/pyinstaller"
|
||||
PACKAGING_DIR="$ROOT/packaging"
|
||||
DESKTOP_API_BASE="http://127.0.0.1:19082"
|
||||
DESKTOP_API_BASE="http://127.0.0.1:9081"
|
||||
SERVER_NAME="capareport-server"
|
||||
cd "$ROOT"
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -235,7 +235,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "capacity-report-desktop"
|
||||
version = "2.0.3"
|
||||
version = "3.0.0"
|
||||
dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "capacity-report-desktop"
|
||||
version = "2.0.3"
|
||||
version = "3.0.0"
|
||||
description = "CapacityReport desktop shell"
|
||||
authors = ["NIXEVOL"]
|
||||
edition = "2021"
|
||||
@@ -9,7 +9,7 @@ edition = "2021"
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["devtools"] }
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-shell = "2"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"identifier": "default",
|
||||
"description": "Default desktop permissions",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default"]
|
||||
"permissions": ["core:default", "core:webview:deny-internal-toggle-devtools"]
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"default":{"identifier":"default","description":"Default desktop permissions","local":true,"windows":["main"],"permissions":["core:default"]}}
|
||||
{"default":{"identifier":"default","description":"Default desktop permissions","local":true,"windows":["main"],"permissions":["core:default","core:webview:deny-internal-toggle-devtools"]}}
|
||||
+26
-3
@@ -13,6 +13,11 @@ use tauri::{Manager, Runtime};
|
||||
use tauri_plugin_shell::process::CommandChild;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
const SERVER_PORT: u16 = 9081;
|
||||
const SERVER_ADDR: &str = "127.0.0.1:9081";
|
||||
#[cfg(target_os = "windows")]
|
||||
const SERVER_PROCESS_NAME: &str = "capareport-server.exe";
|
||||
|
||||
struct ServerProcess {
|
||||
child: CommandChild,
|
||||
pid: u32,
|
||||
@@ -70,8 +75,9 @@ fn start_server<R: Runtime>(app: &tauri::App<R>) -> Result<(), Box<dyn Error>> {
|
||||
fs::create_dir_all(data_dir.join("logs"))?;
|
||||
copy_resource(app, "Configure.json", &data_dir)?;
|
||||
copy_resource(app, "ReportScript.sql", &data_dir)?;
|
||||
stop_existing_server_on_port(19082);
|
||||
stop_existing_server_on_port(SERVER_PORT);
|
||||
|
||||
let port_arg = SERVER_PORT.to_string();
|
||||
let (mut rx, child) = app
|
||||
.shell()
|
||||
.sidecar("capareport-server")?
|
||||
@@ -79,10 +85,10 @@ fn start_server<R: Runtime>(app: &tauri::App<R>) -> Result<(), Box<dyn Error>> {
|
||||
"CAPAREPORT_BASE_DIR",
|
||||
data_dir.to_string_lossy().to_string(),
|
||||
)
|
||||
.args(["--host", "127.0.0.1", "--port", "19082"])
|
||||
.args(["--host", "127.0.0.1", "--port", &port_arg])
|
||||
.spawn()?;
|
||||
|
||||
if let Err(error) = wait_for_server("127.0.0.1:19082", Duration::from_secs(60)) {
|
||||
if let Err(error) = wait_for_server(SERVER_ADDR, Duration::from_secs(60)) {
|
||||
let _ = child.kill();
|
||||
return Err(error);
|
||||
}
|
||||
@@ -139,6 +145,9 @@ fn stop_existing_server_on_port(port: u16) {
|
||||
if pid.parse::<u32>().is_err() {
|
||||
continue;
|
||||
}
|
||||
if !is_capareport_server_pid(pid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let _ = std::process::Command::new("taskkill")
|
||||
.args(["/F", "/T", "/PID", pid])
|
||||
@@ -146,6 +155,20 @@ fn stop_existing_server_on_port(port: u16) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn is_capareport_server_pid(pid: &str) -> bool {
|
||||
let Ok(output) = std::process::Command::new("tasklist")
|
||||
.args(["/FI", &format!("PID eq {pid}"), "/FO", "CSV", "/NH"])
|
||||
.output()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.to_ascii_lowercase()
|
||||
.contains(SERVER_PROCESS_NAME)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn stop_existing_server_on_port(_port: u16) {}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "CapacityReport",
|
||||
"version": "2.0.3",
|
||||
"version": "3.0.0",
|
||||
"identifier": "com.nixevol.capacityreport",
|
||||
"build": {
|
||||
"frontendDist": "../frontend/dist"
|
||||
@@ -14,8 +14,7 @@
|
||||
"width": 1280,
|
||||
"height": 820,
|
||||
"minWidth": 1100,
|
||||
"minHeight": 720,
|
||||
"devtools": true
|
||||
"minHeight": 720
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
@@ -47,6 +46,7 @@
|
||||
},
|
||||
"nsis": {
|
||||
"installerHooks": "./windows/nsis-hooks.nsh",
|
||||
"installMode": "perMachine",
|
||||
"languages": [
|
||||
"SimpChinese"
|
||||
]
|
||||
|
||||
@@ -1,3 +1,26 @@
|
||||
!macro ResolveCapacityReportInstallDir
|
||||
${If} ${FileExists} "D:\*.*"
|
||||
StrCpy $R9 "D:\Program Files\${PRODUCTNAME}"
|
||||
${Else}
|
||||
${If} ${RunningX64}
|
||||
StrCpy $R9 "$PROGRAMFILES64\${PRODUCTNAME}"
|
||||
${Else}
|
||||
StrCpy $R9 "$PROGRAMFILES\${PRODUCTNAME}"
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
!macro NSIS_HOOK_PREINSTALL
|
||||
!insertmacro ResolveCapacityReportInstallDir
|
||||
StrCpy $INSTDIR "$R9"
|
||||
SetOutPath "$INSTDIR"
|
||||
!macroend
|
||||
|
||||
!macro NSIS_HOOK_POSTINSTALL
|
||||
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCTNAME}"
|
||||
DeleteRegKey HKCU "Software\${MANUFACTURER}\${PRODUCTNAME}"
|
||||
!macroend
|
||||
|
||||
!macro NSIS_HOOK_PREUNINSTALL
|
||||
nsExec::ExecToLog 'taskkill /F /T /IM capacity-report-desktop.exe'
|
||||
nsExec::ExecToLog 'taskkill /F /T /IM capareport-server.exe'
|
||||
|
||||
Reference in New Issue
Block a user