核价器
Some checks failed
CI / build (18.x) (push) Failing after 18m45s
CI / build (20.x) (push) Failing after 28m56s
CI / deploy-preview (push) Has been skipped
CI / lint (push) Failing after 16m45s
CI / test (push) Successful in 39m19s
CI / security (push) Successful in 26m10s
Some checks failed
CI / build (18.x) (push) Failing after 18m45s
CI / build (20.x) (push) Failing after 28m56s
CI / deploy-preview (push) Has been skipped
CI / lint (push) Failing after 16m45s
CI / test (push) Successful in 39m19s
CI / security (push) Successful in 26m10s
This commit is contained in:
parent
50cdfb167b
commit
104780d195
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
|||||||
dist/
|
dist/
|
||||||
vite.config.js
|
|
||||||
text.txt
|
text.txt
|
||||||
|
|||||||
26
node_modules/.vite/deps/_metadata.json
generated
vendored
26
node_modules/.vite/deps/_metadata.json
generated
vendored
@ -1,71 +1,71 @@
|
|||||||
{
|
{
|
||||||
"hash": "a0d5344e",
|
"hash": "780fd6b7",
|
||||||
"browserHash": "906d96e7",
|
"browserHash": "11a50aed",
|
||||||
"optimized": {
|
"optimized": {
|
||||||
"@element-plus/icons-vue": {
|
"@element-plus/icons-vue": {
|
||||||
"src": "../../@element-plus/icons-vue/dist/index.js",
|
"src": "../../@element-plus/icons-vue/dist/index.js",
|
||||||
"file": "@element-plus_icons-vue.js",
|
"file": "@element-plus_icons-vue.js",
|
||||||
"fileHash": "b0bff00e",
|
"fileHash": "e73a6617",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"src": "../../axios/index.js",
|
"src": "../../axios/index.js",
|
||||||
"file": "axios.js",
|
"file": "axios.js",
|
||||||
"fileHash": "3661d772",
|
"fileHash": "9ada0434",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"crypto-js": {
|
"crypto-js": {
|
||||||
"src": "../../crypto-js/index.js",
|
"src": "../../crypto-js/index.js",
|
||||||
"file": "crypto-js.js",
|
"file": "crypto-js.js",
|
||||||
"fileHash": "42456c05",
|
"fileHash": "35b4399e",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"dayjs": {
|
"dayjs": {
|
||||||
"src": "../../dayjs/dayjs.min.js",
|
"src": "../../dayjs/dayjs.min.js",
|
||||||
"file": "dayjs.js",
|
"file": "dayjs.js",
|
||||||
"fileHash": "00d3ce60",
|
"fileHash": "3c458ca9",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"element-plus": {
|
"element-plus": {
|
||||||
"src": "../../element-plus/es/index.mjs",
|
"src": "../../element-plus/es/index.mjs",
|
||||||
"file": "element-plus.js",
|
"file": "element-plus.js",
|
||||||
"fileHash": "8289c005",
|
"fileHash": "65c76065",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/locale/lang/zh-cn": {
|
"element-plus/es/locale/lang/zh-cn": {
|
||||||
"src": "../../element-plus/es/locale/lang/zh-cn.mjs",
|
"src": "../../element-plus/es/locale/lang/zh-cn.mjs",
|
||||||
"file": "element-plus_es_locale_lang_zh-cn.js",
|
"file": "element-plus_es_locale_lang_zh-cn.js",
|
||||||
"fileHash": "2b74d0fe",
|
"fileHash": "05b5b909",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"jsbarcode": {
|
"jsbarcode": {
|
||||||
"src": "../../jsbarcode/bin/JsBarcode.js",
|
"src": "../../jsbarcode/bin/JsBarcode.js",
|
||||||
"file": "jsbarcode.js",
|
"file": "jsbarcode.js",
|
||||||
"fileHash": "5774375a",
|
"fileHash": "d60441c9",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"json-bigint": {
|
"json-bigint": {
|
||||||
"src": "../../json-bigint/index.js",
|
"src": "../../json-bigint/index.js",
|
||||||
"file": "json-bigint.js",
|
"file": "json-bigint.js",
|
||||||
"fileHash": "a76f358e",
|
"fileHash": "4eb91e5f",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"pinia": {
|
"pinia": {
|
||||||
"src": "../../pinia/dist/pinia.mjs",
|
"src": "../../pinia/dist/pinia.mjs",
|
||||||
"file": "pinia.js",
|
"file": "pinia.js",
|
||||||
"fileHash": "3c37150a",
|
"fileHash": "461b0d9b",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"vue": {
|
"vue": {
|
||||||
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
||||||
"file": "vue.js",
|
"file": "vue.js",
|
||||||
"fileHash": "c01889f7",
|
"fileHash": "aa9868f1",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"vue-router": {
|
"vue-router": {
|
||||||
"src": "../../vue-router/dist/vue-router.mjs",
|
"src": "../../vue-router/dist/vue-router.mjs",
|
||||||
"file": "vue-router.js",
|
"file": "vue-router.js",
|
||||||
"fileHash": "ced1274b",
|
"fileHash": "010be937",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -250,6 +250,34 @@ export class ServiceManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 注册服务(将 exe 路径注册到启动器)
|
||||||
|
* @param {Object} serviceInfo - { id, name, exe_path, args }
|
||||||
|
*/
|
||||||
|
async registerService(serviceInfo) {
|
||||||
|
this.log('info', `注册服务: ${serviceInfo.id || serviceInfo.exe_path}`);
|
||||||
|
this.log('debug', `POST /api/services`);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.baseUrl}/api/services`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(serviceInfo)
|
||||||
|
});
|
||||||
|
this.log('debug', `响应状态: ${response.status}`);
|
||||||
|
const result = await response.json();
|
||||||
|
this.log('debug', `响应数据: ${JSON.stringify(result)}`);
|
||||||
|
if (result.code === 0) {
|
||||||
|
this.log('success', `注册成功: ${result.msg}`);
|
||||||
|
return result.data || true;
|
||||||
|
}
|
||||||
|
this.log('error', `注册失败: ${result.msg}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
this.log('error', `注册请求失败: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
// ============================================
|
// ============================================
|
||||||
// 自动刷新
|
// 自动刷新
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@ -25,14 +25,16 @@
|
|||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<el-input v-model="dir" placeholder="如 C:\\verifyTool" clearable @clear="dir = ''" />
|
<el-input v-model="dir" placeholder="如 C:\\verifyTool\\kfz-goods-pricing.exe" clearable @clear="dir = ''" />
|
||||||
|
<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="handleOpenExe" size="small">打开程序</el-button>
|
<el-button type="primary" @click="handleBrowseExe" size="small">选择地址</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="通过自定义协议启动本机程序" placement="top" trigger="click">
|
||||||
|
<el-button type="success" @click="handleOpenExe" size="small">打开程序</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -279,6 +281,7 @@ 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,
|
||||||
@ -406,17 +409,166 @@ 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)
|
||||||
|
|
||||||
/** 通过自定义协议启动本地 kfz-goods-pricing.exe */
|
/** 通过本地服务管理器启动核价器 exe */
|
||||||
const handleOpenExe = () => {
|
const fileInputRef = ref(null)
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 注册到启动器并启动
|
||||||
|
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 () => {
|
||||||
if (!dir.value) {
|
if (!dir.value) {
|
||||||
ElMessage.warning({ message: '请先设置程序所在位置', customClass: 'scan-warning-message' })
|
ElMessage.warning({ message: '请先设置程序所在位置', customClass: 'scan-warning-message' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 首选:通过 Vite 开发服务器本地 API 直接启动(无需任何外部脚本/服务)
|
||||||
try {
|
try {
|
||||||
// 调用自定义协议 kfzgs://,launcher.exe 会读取 dir 并启动目标 exe
|
const resp = await fetch('/api/local/launch-exe', {
|
||||||
window.location.href = `kfzgs://launch?dir=${encodeURIComponent(dir.value)}`
|
method: 'POST',
|
||||||
} catch (err: any) {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
ElMessage.error({ message: `启动失败: ${err.message}`, customClass: 'scan-error-message' })
|
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' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -791,6 +943,9 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
dir,
|
dir,
|
||||||
|
fileInputRef,
|
||||||
|
handleBrowseExe,
|
||||||
|
handleFileSelected,
|
||||||
ip,
|
ip,
|
||||||
port,
|
port,
|
||||||
verifyIndex,
|
verifyIndex,
|
||||||
|
|||||||
@ -70,6 +70,13 @@
|
|||||||
<el-button type="primary" @click="handlePrintBarcode('Alt+b')">打印条码</el-button>
|
<el-button type="primary" @click="handlePrintBarcode('Alt+b')">打印条码</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="提交快捷键">
|
||||||
|
<div class="printer-row">
|
||||||
|
<el-input :model-value="'Alt+r'" disabled style="width: 200px" />
|
||||||
|
<img v-if="barcodeImages['Alt+r']" :src="barcodeImages['Alt+r']" />
|
||||||
|
<el-button type="primary" @click="handlePrintBarcode('Alt+r')">打印条码</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
|
|
||||||
@ -128,7 +135,7 @@ const cameraLoading = ref(false)
|
|||||||
const cameras = ref([])
|
const cameras = ref([])
|
||||||
const selectedCamera = ref('')
|
const selectedCamera = ref('')
|
||||||
|
|
||||||
const shortcutItems = ['Alt+c', 'Alt+a', 'Alt+x', 'Alt+b']
|
const shortcutItems = ['Alt+c', 'Alt+a', 'Alt+x', 'Alt+b', 'Alt+r']
|
||||||
const barcodeImages = reactive({})
|
const barcodeImages = reactive({})
|
||||||
|
|
||||||
shortcutItems.forEach(key => {
|
shortcutItems.forEach(key => {
|
||||||
@ -191,6 +198,9 @@ const confirmPaperSize = () => {
|
|||||||
if (paperSize.value) {
|
if (paperSize.value) {
|
||||||
localStorage.setItem(STORAGE_KEY_PAPER_SIZE, paperSize.value)
|
localStorage.setItem(STORAGE_KEY_PAPER_SIZE, paperSize.value)
|
||||||
ElMessage.success({ message: '小票纸大小已保存', duration: 1000, customClass: 'scan-success-message' })
|
ElMessage.success({ message: '小票纸大小已保存', duration: 1000, customClass: 'scan-success-message' })
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(STORAGE_KEY_PAPER_SIZE)
|
||||||
|
ElMessage.success({ message: '小票纸大小已清除', duration: 1000, customClass: 'scan-success-message' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,16 +216,26 @@ const confirmExpressPrinter = () => {
|
|||||||
|
|
||||||
const fetchCameras = async () => {
|
const fetchCameras = async () => {
|
||||||
cameraLoading.value = true
|
cameraLoading.value = true
|
||||||
|
let stream = null
|
||||||
try {
|
try {
|
||||||
|
if (!navigator.mediaDevices) {
|
||||||
|
throw new Error('当前环境不支持摄像头')
|
||||||
|
}
|
||||||
|
// HTTP 环境下需先获取摄像头权限再枚举设备
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia({ video: true })
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices()
|
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||||
cameras.value = devices.filter((d) => d.kind === 'videoinput').map((d) => ({
|
cameras.value = devices.filter((d) => d.kind === 'videoinput').map((d) => ({
|
||||||
deviceId: d.deviceId,
|
deviceId: d.deviceId,
|
||||||
label: d.label || `摄像头 ${d.deviceId.slice(0, 8)}`
|
label: d.label || `摄像头 ${d.deviceId.slice(0, 8)}`
|
||||||
}))
|
}))
|
||||||
} catch {
|
} catch (err) {
|
||||||
ElMessage.error({ message: '获取摄像头列表失败', customClass: 'scan-error-message' })
|
console.warn('[摄像头] 获取列表失败:', err)
|
||||||
|
ElMessage.error({ message: '获取摄像头列表失败: ' + (err?.message || '未知错误'), customClass: 'scan-error-message' })
|
||||||
cameras.value = []
|
cameras.value = []
|
||||||
} finally {
|
} finally {
|
||||||
|
if (stream) {
|
||||||
|
stream.getTracks().forEach(track => track.stop())
|
||||||
|
}
|
||||||
cameraLoading.value = false
|
cameraLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
245
vite.config.js
Normal file
245
vite.config.js
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
const { defineConfig } = require('vite')
|
||||||
|
const vue = require('@vitejs/plugin-vue').default
|
||||||
|
const path = require('path')
|
||||||
|
const { spawn } = require('child_process')
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
module.exports = defineConfig({
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
silenceDeprecations: ['legacy-js-api'],
|
||||||
|
api: 'modern-compiler'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
// 孔夫子旧书网登录代理(避免浏览器跨域限制)
|
||||||
|
{
|
||||||
|
name: 'kongfz-login-proxy',
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use('/kongfz-login', async (req, res) => {
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
res.end(JSON.stringify({ code: 405, message: '仅支持 POST 请求' }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = ''
|
||||||
|
req.on('data', chunk => { body += chunk.toString() })
|
||||||
|
req.on('end', async () => {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams(body)
|
||||||
|
const username = params.get('loginName') || ''
|
||||||
|
const password = params.get('loginPass') || ''
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
res.end(JSON.stringify({ code: 400, message: '请输入用户名和密码!' }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const https = require('https')
|
||||||
|
const http = require('http')
|
||||||
|
|
||||||
|
function requestWithRedirect(options, bodyData, redirectCount = 0) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (redirectCount > 5) {
|
||||||
|
reject(new Error('重定向次数过多'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const mod = options.port === 443 ? https : http
|
||||||
|
const req = mod.request(options, (response) => {
|
||||||
|
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
||||||
|
const redirectUrl = new URL(response.headers.location, `https://${options.hostname}`)
|
||||||
|
const redirectOpts = {
|
||||||
|
hostname: redirectUrl.hostname,
|
||||||
|
port: redirectUrl.port || (redirectUrl.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: redirectUrl.pathname + redirectUrl.search,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'
|
||||||
|
},
|
||||||
|
timeout: 15000
|
||||||
|
}
|
||||||
|
if (response.headers['set-cookie']) {
|
||||||
|
const cookies = Array.isArray(response.headers['set-cookie'])
|
||||||
|
? response.headers['set-cookie'].join('; ')
|
||||||
|
: response.headers['set-cookie']
|
||||||
|
redirectOpts.headers['Cookie'] = cookies.split(';')[0].trim()
|
||||||
|
}
|
||||||
|
resolve(requestWithRedirect(redirectOpts, null, redirectCount + 1))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let responseData = ''
|
||||||
|
response.on('data', chunk => { responseData += chunk })
|
||||||
|
response.on('end', () => resolve({ statusCode: response.statusCode, headers: response.headers, body: responseData }))
|
||||||
|
})
|
||||||
|
req.on('error', err => reject(new Error(`登录请求失败: ${err.message}`)))
|
||||||
|
req.on('timeout', () => { req.destroy(); reject(new Error('登录请求超时')) })
|
||||||
|
if (bodyData) req.write(bodyData)
|
||||||
|
req.end()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new URLSearchParams()
|
||||||
|
formData.append('loginName', username)
|
||||||
|
formData.append('loginPass', password)
|
||||||
|
formData.append('returnUrl', 'http://user.kongfz.com/')
|
||||||
|
const formDataStr = formData.toString()
|
||||||
|
|
||||||
|
const loginResponse = await requestWithRedirect({
|
||||||
|
hostname: 'login.kongfz.com',
|
||||||
|
port: 443,
|
||||||
|
path: '/Pc/Login/account',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Content-Length': Buffer.byteLength(formDataStr),
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'
|
||||||
|
},
|
||||||
|
timeout: 15000
|
||||||
|
}, formDataStr)
|
||||||
|
|
||||||
|
if (loginResponse.statusCode !== 200) {
|
||||||
|
throw new Error(`登录失败(HTTP状态码: ${loginResponse.statusCode})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setCookie = loginResponse.headers['set-cookie'] || []
|
||||||
|
const cookieStr = Array.isArray(setCookie) ? setCookie.join('; ') : setCookie || ''
|
||||||
|
|
||||||
|
if (loginResponse.body.includes("window.location.href='https://login.kongfz.cn/Pc/Session/rsync")) {
|
||||||
|
if (!cookieStr) throw new Error('登录成功但未获取到Cookie')
|
||||||
|
if (!cookieStr.includes('PHPSESSID=')) throw new Error('登录失败: 未找到 PHPSESSID')
|
||||||
|
|
||||||
|
const token = cookieStr.split('PHPSESSID=')[1].split(';')[0]
|
||||||
|
|
||||||
|
const userInfoResponse = await requestWithRedirect({
|
||||||
|
hostname: 'user.kongfz.com',
|
||||||
|
port: 443,
|
||||||
|
path: '/User/Index/getUserInfo/',
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Cookie': `PHPSESSID=${token}`,
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||||
|
'Accept': 'application/json, text/plain, */*',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
|
||||||
|
},
|
||||||
|
timeout: 15000
|
||||||
|
}, null)
|
||||||
|
|
||||||
|
let displayName = username
|
||||||
|
try {
|
||||||
|
const userJson = JSON.parse(userInfoResponse.body)
|
||||||
|
if (userJson.status && userJson.data) displayName = userJson.data.nickname || username
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
code: 200, message: '登录成功',
|
||||||
|
data: { token, username: displayName, nickname: displayName }
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const errJson = JSON.parse(loginResponse.body)
|
||||||
|
if (errJson.errCode === 1001 || errJson.errCode === 1005) throw new Error('账号或密码错误')
|
||||||
|
throw new Error(errJson.errInfo || '登录失败,未知错误!')
|
||||||
|
} catch (parseErr) {
|
||||||
|
if (parseErr instanceof SyntaxError) throw new Error('登录失败,未知错误!')
|
||||||
|
throw parseErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
res.end(JSON.stringify({ code: 500, message: error.message }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 本地程序启动器(无需协议注册/脚本/额外服务,Vite 服务器直接 spawn exe)
|
||||||
|
{
|
||||||
|
name: 'local-launcher',
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use('/api/local/launch-exe', (req, res) => {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.statusCode = 204
|
||||||
|
return res.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
return res.end(JSON.stringify({ code: 405, msg: '仅支持 POST' }))
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = ''
|
||||||
|
req.on('data', chunk => { body += chunk.toString() })
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
const { exe_path } = JSON.parse(body)
|
||||||
|
if (!exe_path) {
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
return res.end(JSON.stringify({ code: 400, msg: '缺少 exe_path' }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
if (!fs.existsSync(exe_path)) {
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
return res.end(JSON.stringify({ code: 404, msg: `文件不存在: ${exe_path}` }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const exeDir = path.dirname(exe_path)
|
||||||
|
const proc = spawn(exe_path, [], {
|
||||||
|
cwd: exeDir,
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore',
|
||||||
|
windowsHide: false
|
||||||
|
})
|
||||||
|
proc.unref()
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
res.end(JSON.stringify({ code: 0, msg: '已启动', pid: proc.pid }))
|
||||||
|
} catch (err) {
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
res.end(JSON.stringify({ code: 500, msg: err.message }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false'
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5174,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
historyApiFallback: true,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
// target: 'http://127.0.0.1:9090',
|
||||||
|
// target: 'http://192.168.101.213:9090',
|
||||||
|
target: 'https://psi.api.buzhiyushu.cn',
|
||||||
|
// target: 'https://psi.api.buzhiyushu.cn',
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/api/print': {
|
||||||
|
// target: 'http://192.168.101.127:8075',
|
||||||
|
target: 'https://print.buzhiyushu.cn',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user