程序所在位置
Some checks failed
CI / build (18.x) (push) Failing after 31m9s
CI / build (20.x) (push) Failing after 1m34s
CI / deploy-preview (push) Has been skipped
CI / lint (push) Failing after 1m35s
CI / test (push) Failing after 33s
CI / security (push) Failing after 34s

This commit is contained in:
97694731 2026-06-26 16:51:51 +08:00
parent 104780d195
commit 520712bfab
10 changed files with 301 additions and 167 deletions

View File

@ -373,7 +373,7 @@ onMounted(() => {
loadTodayStats() loadTodayStats()
loadLatestProducts() loadLatestProducts()
loadEmployeeStats() loadEmployeeStats()
// loadStoreInfo() loadStoreInfo()
}) })
</script> </script>

View File

@ -25,12 +25,11 @@
</el-tooltip> </el-tooltip>
</span> </span>
</template> </template>
<el-input v-model="dir" placeholder="如 C:\\verifyTool\\kfz-goods-pricing.exe" clearable @clear="dir = ''" /> <el-input v-model="dir" placeholder="如 C:\Users\pc\Desktop\price" />
<input ref="fileInputRef" type="file" accept=".exe" style="display: none" @change="handleFileSelected" />
<div class="save-bar" style="margin-top: 18px;"> <div class="save-bar" style="margin-top: 18px;">
<el-tooltip content="选择本机程序文件" placement="top" trigger="click"> <el-tooltip content="保存核价器所在目录" placement="top" trigger="click">
<el-button type="primary" @click="handleBrowseExe" size="small">选择地址</el-button> <el-button type="primary" @click="handleSaveDir" size="small">保存</el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="通过自定义协议启动本机程序" placement="top" trigger="click"> <el-tooltip content="通过自定义协议启动本机程序" placement="top" trigger="click">
<el-button type="success" @click="handleOpenExe" size="small">打开程序</el-button> <el-button type="success" @click="handleOpenExe" size="small">打开程序</el-button>
@ -281,7 +280,6 @@ import { ref, onMounted } from 'vue'
import axios from 'axios' import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Plus, Setting, User, Link, VideoPlay } from '@element-plus/icons-vue' import { Delete, Plus, Setting, User, Link, VideoPlay } from '@element-plus/icons-vue'
import { ServiceManager } from '@/utils/ServiceManager'
import { import {
testConnection, testConnection,
kongfzLogin, kongfzLogin,
@ -409,167 +407,16 @@ export default {
const bindLoading = ref(false) const bindLoading = ref(false)
const result = ref<{ success: boolean; message: string } | null>(null) const result = ref<{ success: boolean; message: string } | null>(null)
/** 通过本地服务管理器启动核价器 exe */ /** 保存核价器所在目录到 localStorage */
const fileInputRef = ref(null) const handleSaveDir = () => {
const serviceManager = new ServiceManager('http://127.0.0.1:5000')
const SERVICE_ID = 'kfz-goods-pricing'
const handleBrowseExe = () => {
fileInputRef.value?.click()
}
const handleFileSelected = async () => {
const input = fileInputRef.value
if (!input?.files?.length) return
const file = input.files[0]
// file.path
let fullPath = (file as any).path || ''
// ServiceManager
if (!fullPath || !(fullPath.includes('\\') || fullPath.includes('/'))) {
try {
const services = await serviceManager.getServicesStatus()
const matched = services.find(s => s.exe_path && s.exe_path.includes(file.name))
if (matched) {
fullPath = matched.exe_path
}
} catch {
// ServiceManager
}
}
dir.value = fullPath || file.name
localStorage.setItem(STORAGE_KEY_FILE_DIR, dir.value) localStorage.setItem(STORAGE_KEY_FILE_DIR, dir.value)
ElMessage.success({ message: '路径已保存', duration: 1000, customClass: 'scan-success-message' })
//
if (fullPath && (fullPath.includes('\\') || fullPath.includes('/'))) {
const exeName = file.name.replace(/\.exe$/i, '')
try {
await serviceManager.registerService({
id: SERVICE_ID,
name: '核价器',
exe_path: fullPath,
})
ElMessage.success('已注册到启动器')
} catch {
//
}
// Vite API
try {
const resp = await fetch('/api/local/launch-exe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ exe_path: fullPath })
})
const result = await resp.json()
if (result.code === 0) {
ElMessage.success({ message: `核价器已启动 (PID ${result.pid})`, duration: 1000, customClass: 'scan-success-message' })
} else {
// ServiceManager
await serviceManager.startService(SERVICE_ID)
}
} catch {
// Vite API ServiceManager
try {
await serviceManager.startService(SERVICE_ID)
} catch { /* 无法启动 */ }
}
}
// input 便 change
input.value = ''
} }
const handleOpenExe = async () => { /** 通过自定义协议启动核价器 exe */
if (!dir.value) { const handleOpenExe = () => {
ElMessage.warning({ message: '请先设置程序所在位置', customClass: 'scan-warning-message' }) window.location.href = 'kfzprice://launch'
return ElMessage.success({ message: '启动指令已发送', duration: 1000, customClass: 'scan-success-message' })
}
// Vite API /
try {
const resp = await fetch('/api/local/launch-exe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ exe_path: dir.value })
})
const result = await resp.json()
if (result.code === 0) {
ElMessage.success({ message: `核价器已启动 (PID ${result.pid})`, duration: 1000, customClass: 'scan-success-message' })
return
}
console.warn('[启动程序] Vite 本地 API 失败:', result.msg)
} catch {
// Vite API
}
try {
//
const services = await serviceManager.getServicesStatus()
if (services.length > 0) {
let target = null
// exe_path SERVICE_ID
const isFullPath = dir.value.includes('\\') || dir.value.includes('/')
if (isFullPath) {
target = services.find(s => s.exe_path && s.exe_path.includes(dir.value))
}
if (!target) {
target = services.find(s => s.id === SERVICE_ID)
}
if (target) {
const ok = await serviceManager.startService(target.id)
if (ok) {
ElMessage.success({ message: `已启动: ${target.name || target.id}`, duration: 1000, customClass: 'scan-success-message' })
return
}
}
}
// 退
console.warn('[启动程序] 服务管理器不可用,回退到自定义协议')
// exe
const lastSep = Math.max(dir.value.lastIndexOf('\\'), dir.value.lastIndexOf('/'))
const dirPath = lastSep > 0 ? dir.value.substring(0, lastSep) : dir.value
const exeName = lastSep > 0 ? dir.value.substring(lastSep + 1) : ''
let launched = false
// 1. kfzgs://
try {
if (exeName) {
window.location.href = `kfzgs://launch?dir=${encodeURIComponent(dirPath)}&exe=${encodeURIComponent(exeName)}`
} else {
window.location.href = `kfzgs://launch?dir=${encodeURIComponent(dirPath)}`
}
launched = true
} catch { /* ignore */ }
// 2. file:// + window.open
if (!launched) {
const fileUrl = `file:///${dir.value.replace(/\\/g, '/')}`
const win = window.open(fileUrl, '_blank')
if (!win) {
// a
const a = document.createElement('a')
a.href = fileUrl
a.target = '_blank'
a.click()
}
}
ElMessage.success({ message: '启动指令已发送', duration: 1000, customClass: 'scan-success-message' })
} catch (err) {
// 退
console.warn('[启动程序] 服务管理器连接失败:', err)
try {
const fileUrl = `file:///${dir.value.replace(/\\/g, '/')}`
const win = window.open(fileUrl, '_blank')
if (!win) {
const a = document.createElement('a')
a.href = fileUrl
a.target = '_blank'
a.click()
}
ElMessage.success({ message: '启动指令已发送', duration: 1000, customClass: 'scan-success-message' })
} catch (err2) {
ElMessage.error({ message: `启动失败: ${err2.message || '未知错误'}`, customClass: 'scan-error-message' })
}
}
} }
/** 将单个账号写入已保存列表 */ /** 将单个账号写入已保存列表 */
@ -943,9 +790,7 @@ export default {
return { return {
dir, dir,
fileInputRef, handleSaveDir,
handleBrowseExe,
handleFileSelected,
ip, ip,
port, port,
verifyIndex, verifyIndex,

View File

@ -1,4 +1,14 @@
<template> <template>
<div style="margin-bottom: 16px">
<el-steps :active="1" align-center>
<el-step title="销售订单" />
<el-step title="出库管理" />
<el-step title="发货单" />
</el-steps>
<div style="text-align: right; margin-top: 12px">
<el-button type="primary" @click="$router.push('/shipping-order')">下一步</el-button>
</div>
</div>
<el-card class="outbound-manager"> <el-card class="outbound-manager">
<template #header> <template #header>
<div class="card-header">出库单管理</div> <div class="card-header">出库单管理</div>

View File

@ -1,7 +1,18 @@
<template> <template>
<div style="margin-bottom: 16px">
<el-steps :active="0" align-center>
<el-step title="销售订单" />
<el-step title="出库管理" />
<el-step title="发货单" />
</el-steps>
<div style="text-align: right; margin-top: 12px">
<el-button type="primary" @click="$router.push('/outbound')">下一步</el-button>
</div>
</div>
<el-card class="sales-order-manager"> <el-card class="sales-order-manager">
<template #header> <template #header>
<div class="card-header">销售订单管理</div> <div class="card-header">销售订单管理</div>
</template> </template>
<div class="filter-bar"> <div class="filter-bar">
<el-input v-model="searchParams.keyword" placeholder="销售订单号" clearable style="width: 220px" <el-input v-model="searchParams.keyword" placeholder="销售订单号" clearable style="width: 220px"

View File

@ -1,4 +1,9 @@
<template> <template>
<el-steps :active="2" align-center style="margin-bottom: 16px">
<el-step title="销售订单" />
<el-step title="出库管理" />
<el-step title="发货单" />
</el-steps>
<div class="shipping-order-container"> <div class="shipping-order-container">
<!-- 左侧卡片商品详情 --> <!-- 左侧卡片商品详情 -->
<el-card class="left-card" shadow="never"> <el-card class="left-card" shadow="never">

View File

@ -0,0 +1,11 @@
@echo off
chcp 65001 >nul
cd /d "%~dp0"
set "LAUNCHER=%~dp0kfzgs-launcher.ps1"
echo 启动核价器后台服务(端口 5000...
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%LAUNCHER%" 2>&1
echo 服务已退出
pause

View File

@ -0,0 +1,188 @@
# kfzgs-launcher.ps1 - 核价器启动脚本 (便携版)
# 用法:
# 协议模式: powershell -File kfzgs-launcher.ps1 -url "kfzgs://launch?dir=..."
# 服务模式: powershell -File kfzgs-launcher.ps1
param([string]$url = "")
$SERVICE_ID = "kfz-goods-pricing"
$HTTP_PORT = 5000
$SCRIPT_DIR = Split-Path $MyInvocation.MyCommand.Path -Parent
# 从同目录下的 config.json 读取 exe 路径,没有则用同目录下默认 exe
$CONFIG_FILE = Join-Path $SCRIPT_DIR "launcher-config.json"
$DEFAULT_EXE = Join-Path $SCRIPT_DIR "kfz-goods-pricing.exe"
function Load-Config {
if (Test-Path $CONFIG_FILE) {
try {
$config = Get-Content $CONFIG_FILE -Raw | ConvertFrom-Json
if ($config.exe_path) { return $config.exe_path }
} catch {}
}
return $DEFAULT_EXE
}
function Save-Config($exePath) {
@{ exe_path = $exePath; id = $SERVICE_ID; name = "核价器" } | ConvertTo-Json | Out-File $CONFIG_FILE -Encoding UTF8
}
$EXE_PATH = Load-Config
function Parse-KfzgsUrl($rawUrl) {
$decoded = [System.Uri]::UnescapeDataString($rawUrl)
$stripped = $decoded -replace "^kfzgs://", ""
$action = ""
$params = @{}
if ($stripped -match "^([^?]+)\?(.*)$") {
$action = $Matches[1]
$query = $Matches[2]
$query -split "&" | ForEach-Object {
$parts = $_ -split "=", 2
if ($parts.Count -eq 2) { $params[$parts[0]] = $parts[1] }
}
} elseif ($stripped -notmatch "[?]") {
$action = $stripped
}
return @{ action = $action; params = $params }
}
function Invoke-ProtocolHandler($rawUrl) {
$parsed = Parse-KfzgsUrl $rawUrl
$p = $parsed.params
if ($parsed.action -eq "launch") {
$dir = $p["dir"] -replace "/", "\"
$exeName = $p["exe"]
if ($exeName) {
if ($dir) {
if ($dir.EndsWith("\")) { $fullPath = "$dir$exeName" }
else { $fullPath = "$dir\$exeName" }
} else {
$fullPath = Join-Path $SCRIPT_DIR $exeName
}
} elseif ($dir -and $dir -match "\.exe$") {
if (Test-Path $dir) { $fullPath = $dir }
else { $fullPath = Join-Path $SCRIPT_DIR $dir }
} else { $fullPath = $EXE_PATH }
if ($fullPath -ne $EXE_PATH) {
Save-Config $fullPath
$script:EXE_PATH = $fullPath
}
if (Test-Path $fullPath) {
$parentDir = Split-Path $fullPath -Parent
Start-Process -FilePath $fullPath -WorkingDirectory $parentDir -WindowStyle Normal
}
}
}
function Start-ApiServer {
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add("http://127.0.0.1:$HTTP_PORT/")
try { $listener.Start() } catch { exit 1 }
$services = @{}
$services[$SERVICE_ID] = @{
id = $SERVICE_ID
name = "核价器"
exe_path = $EXE_PATH
running = $false
}
while ($listener.IsListening) {
$context = $listener.GetContext()
$request = $context.Request
$response = $context.Response
if ($request.HttpMethod -eq "OPTIONS") {
$response.AddHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
$response.AddHeader("Access-Control-Allow-Headers", "Content-Type")
$response.StatusCode = 204
$response.Close()
continue
}
$path = $request.Url.AbsolutePath
$result = @{ code = 0; msg = "ok" }
try {
if ($path -eq "/api/services") {
if ($request.HttpMethod -eq "GET") {
$services[$SERVICE_ID]["exe_path"] = $EXE_PATH
$result["data"] = @($services.Values)
} elseif ($request.HttpMethod -eq "POST") {
$reader = New-Object System.IO.StreamReader($request.InputStream)
$body = $reader.ReadToEnd() | ConvertFrom-Json
if ($body.id -and $body.exe_path) {
$services[$body.id] = @{
id = $body.id
name = if ($body.name) { $body.name } else { $body.id }
exe_path = $body.exe_path
running = $false
}
Save-Config $body.exe_path
$script:EXE_PATH = $body.exe_path
$result["msg"] = "已注册: $($body.id)"
$result["data"] = $services[$body.id]
} else {
$result["code"] = 1
$result["msg"] = "缺少 id 或 exe_path"
}
}
}
elseif ($path -match "^/api/services/([^/]+)$" -and $request.HttpMethod -eq "GET") {
$sid = $Matches[1]
if ($services.ContainsKey($sid)) {
$result["data"] = $services[$sid]
} else {
$result["code"] = 1; $result["msg"] = "未找到"
}
}
elseif ($path -match "^/api/services/([^/]+)/start$" -and $request.HttpMethod -eq "POST") {
$sid = $Matches[1]
if ($services.ContainsKey($sid)) {
$svc = $services[$sid]
$exePath = if ($svc["exe_path"] -and (Test-Path $svc["exe_path"])) { $svc["exe_path"] } else { $EXE_PATH }
if (Test-Path $exePath) {
$parentDir = Split-Path $exePath -Parent
$proc = Start-Process -FilePath $exePath -WorkingDirectory $parentDir -WindowStyle Normal -PassThru
$svc["running"] = $true
$result["msg"] = "已启动"
$result["pid"] = $proc.Id
} else { $result["code"] = 1; $result["msg"] = "文件不存在: $exePath" }
} else { $result["code"] = 1; $result["msg"] = "未找到" }
}
elseif ($path -match "^/api/services/([^/]+)/stop$" -and $request.HttpMethod -eq "POST") {
$sid = $Matches[1]
if ($services.ContainsKey($sid)) {
$killed = $false
Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -eq $services[$sid]["exe_path"] } | ForEach-Object {
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
$killed = $true
}
$services[$sid]["running"] = $false
$result["msg"] = if ($killed) { "已停止" } else { "未在运行" }
$result["killed"] = $killed
} else { $result["code"] = 1; $result["msg"] = "未找到" }
}
else { $result["code"] = 404; $result["msg"] = "未知接口" }
} catch {
$result["code"] = 1; $result["msg"] = $_.Exception.Message
}
$json = $result | ConvertTo-Json -Compress -Depth 3
$buffer = [System.Text.Encoding]::UTF8.GetBytes($json)
$response.AddHeader("Access-Control-Allow-Origin", "*")
$response.ContentType = "application/json; charset=utf-8"
$response.ContentLength64 = $buffer.Length
$response.OutputStream.Write($buffer, 0, $buffer.Length)
$response.Close()
}
}
if ($url -and $url -match "^kfzgs://") {
Invoke-ProtocolHandler $url
} else {
Start-ApiServer
}

View File

@ -0,0 +1 @@
{"exe_path": "D:\\project\\psi_web\\verifyTool\\kfz-goods-pricing.exe","id": "kfz-goods-pricing","name": "核价器"}

47
verifyTool/setup.bat Normal file
View File

@ -0,0 +1,47 @@
@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion
title 核价器协议注册
set "PROTOCOL=kfzgs"
set "EXE_NAME=kfz-goods-pricing.exe"
set "SD=%~dp0"
set "SD=%SD:~0,-1%"
set "EXE=%EXE_NAME%"
if not "%EXE:~1,2%"==":\" set "EXE=%SD%\%EXE%"
if not exist "%EXE%" (
echo [ERR] 未找到: %EXE%
pause
goto :eof
)
for %%A in ("%EXE%") do set "EXE_DIR=%%~dpA"
set "EXE_DIR=%EXE_DIR:~0,-1%"
echo ============================================
echo 核价器协议注册
echo ============================================
echo Protocol: %PROTOCOL%://
echo EXE: %EXE%
echo WorkDir: %EXE_DIR%
echo ============================================
echo.
reg delete "HKCR\%PROTOCOL%" /f >nul 2>&1
reg add "HKCR\%PROTOCOL%" /ve /d "URL:%PROTOCOL% Protocol" /f >nul
reg add "HKCR\%PROTOCOL%" /v "URL Protocol" /d "" /f >nul
reg add "HKCR\%PROTOCOL%\DefaultIcon" /ve /d "\"%EXE%\",0" /f >nul
reg add "HKCR\%PROTOCOL%\shell\open\command" /ve /d "cmd /c start \"\" /D \"%EXE_DIR%\" \"%EXE%\" \"%%1\"" /f >nul
if !errorlevel! equ 0 (
echo [OK] %PROTOCOL%:// 注册成功
echo 测试: Win+R ^> %PROTOCOL%://launch
) else (
echo [FAIL] 请右键以管理员身份运行
)
echo.
pause

View File

@ -212,6 +212,22 @@ module.exports = defineConfig({
} }
}) })
}) })
server.middlewares.use('/api/local/exe-config', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Content-Type', 'application/json')
if (req.method !== 'GET') return res.end(JSON.stringify({ code: 405 }))
try {
const { execSync } = require('child_process')
const cmd = 'powershell -NoProfile -Command "(Get-ItemProperty -Path \'Registry::HKEY_CLASSES_ROOT\\kfzprice\\shell\\open\\command\' -Name \'(default)\').\'(default)\'"'
const raw = execSync(cmd, { encoding: 'utf8', timeout: 5000 }).trim()
const match = raw.match(/"([^"]+\.exe)"/i)
res.end(JSON.stringify({ code: 0, data: { exe_path: match ? match[1] : '' } }))
} catch (err) {
res.end(JSON.stringify({ code: 500, msg: err.message }))
}
})
} }
} }
], ],