Merge remote-tracking branch 'origin/fix-error' into fix-error
This commit is contained in:
14
package-lock.json
generated
14
package-lock.json
generated
@@ -14,6 +14,8 @@
|
|||||||
"bonjour-service": "^1.3.0",
|
"bonjour-service": "^1.3.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"element-plus": "^2.13.6",
|
"element-plus": "^2.13.6",
|
||||||
|
"js-base64": "3.7.5",
|
||||||
|
"jsencrypt": "^3.5.4",
|
||||||
"lucide-vue-next": "^1.0.0",
|
"lucide-vue-next": "^1.0.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.32",
|
"vue": "^3.5.32",
|
||||||
@@ -6919,6 +6921,12 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-base64": {
|
||||||
|
"version": "3.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz",
|
||||||
|
"integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -6939,6 +6947,12 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsencrypt": {
|
||||||
|
"version": "3.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsencrypt/-/jsencrypt-3.5.4.tgz",
|
||||||
|
"integrity": "sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/json-buffer": {
|
"node_modules/json-buffer": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||||
|
|||||||
@@ -49,7 +49,9 @@
|
|||||||
"lucide-vue-next": "^1.0.0",
|
"lucide-vue-next": "^1.0.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.32",
|
"vue": "^3.5.32",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4",
|
||||||
|
"js-base64": "3.7.5",
|
||||||
|
"jsencrypt": "^3.5.4"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,vue}": [
|
"*.{js,vue}": [
|
||||||
|
|||||||
@@ -11,6 +11,16 @@
|
|||||||
<p class="text-base font-semibold text-gray-700 mb-6">登录后体验更多功能</p>
|
<p class="text-base font-semibold text-gray-700 mb-6">登录后体验更多功能</p>
|
||||||
|
|
||||||
<el-form :model="form" :rules="rules" ref="formRef" class="w-full" @submit.prevent="handleLogin">
|
<el-form :model="form" :rules="rules" ref="formRef" class="w-full" @submit.prevent="handleLogin">
|
||||||
|
<el-form-item prop="sparkDevice">
|
||||||
|
<el-select v-model="form.sparkDevice" placeholder="请选择设备" size="large" class="w-full" :no-data-text="'暂未发现设备,请先扫描'">
|
||||||
|
<el-option
|
||||||
|
v-for="device in sparkStore.devices"
|
||||||
|
:key="device.name"
|
||||||
|
:label="`${device.name} (${device.addresses?.[0]}:${device.port})`"
|
||||||
|
:value="device.name"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<el-input v-model="form.username" placeholder="请输入账号" :prefix-icon="User" size="large" />
|
<el-input v-model="form.username" placeholder="请输入账号" :prefix-icon="User" size="large" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -56,6 +66,10 @@
|
|||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { User } from '@element-plus/icons-vue';
|
import { User } from '@element-plus/icons-vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { useSparkStore } from '@/stores/spark';
|
||||||
|
import { loginAction } from '@/http/api.js';
|
||||||
|
|
||||||
|
const sparkStore = useSparkStore();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -78,11 +92,13 @@ const loading = ref(false);
|
|||||||
const agreed = ref(false);
|
const agreed = ref(false);
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
|
sparkDevice: sparkStore.selectedDevice?.name || '',
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
|
sparkDevice: [{ required: true, message: '请选择设备', trigger: 'change' }],
|
||||||
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
|
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
|
||||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||||
};
|
};
|
||||||
@@ -96,10 +112,16 @@ async function handleLogin() {
|
|||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
// TODO: 替换为真实登录接口
|
const device = sparkStore.devices.find((d) => d.name === form.value.sparkDevice);
|
||||||
await new Promise((r) => setTimeout(r, 800));
|
if (device) sparkStore.selectDevice(device);
|
||||||
ElMessage.success('登录成功');
|
|
||||||
emit('login-success', { username: form.value.username });
|
const url = sparkStore.selectedDeviceUrl;
|
||||||
|
console.log('[Login] spark device:', device);
|
||||||
|
console.log('[Login] target url:', url);
|
||||||
|
|
||||||
|
await loginAction({ email: form.value.username, password: form.value.password }, url);
|
||||||
|
ElMessage.success(`登录成功 | ${url ?? '未选择设备'}`);
|
||||||
|
emit('login-success', { username: form.value.username, device });
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ElMessage.error('登录失败,请重试');
|
ElMessage.error('登录失败,请重试');
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { getAction, postAction, deleteAction } from './manage.js';
|
import { getAction, postAction, deleteAction } from './manage.js';
|
||||||
import url, { getBaseUrl } from './url.js';
|
import url, { getBaseUrl } from './url.js';
|
||||||
|
import { encryptPassword } from '@/utils/crypto.js';
|
||||||
|
|
||||||
// 健康检查
|
// 健康检查
|
||||||
export const getHealthAction = () => getAction(url.health);
|
export const getHealthAction = () => getAction(url.health);
|
||||||
|
|
||||||
|
// 用户登录
|
||||||
|
export const loginAction = (data, sparkBaseUrl) =>
|
||||||
|
postAction(url.user.login, { email: data.email, password: encryptPassword(data.password) }, {}, sparkBaseUrl);
|
||||||
|
|
||||||
// 会话
|
// 会话
|
||||||
export const createSessionAction = (data) => postAction(url.session.create, data);
|
export const createSessionAction = (data) => postAction(url.session.create, data);
|
||||||
export const getSessionAction = (id) => getAction(url.session.detail(id));
|
export const getSessionAction = (id) => getAction(url.session.detail(id));
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import request from './index.js'
|
import request from './index.js';
|
||||||
|
|
||||||
export function getAction(url, params) {
|
export function getAction(url, params) {
|
||||||
return request({ url, method: 'GET', params })
|
return request({ url, method: 'GET', params });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postAction(url, data, headers = {}) {
|
export function postAction(url, data, headers = {}, baseURL) {
|
||||||
return request({ url, method: 'POST', data, headers })
|
return request({ url, method: 'POST', data, headers, ...(baseURL ? { baseURL } : {}) });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function putAction(url, data) {
|
export function putAction(url, data) {
|
||||||
return request({ url, method: 'PUT', data })
|
return request({ url, method: 'PUT', data });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteAction(url, params) {
|
export function deleteAction(url, params) {
|
||||||
return request({ url, method: 'DELETE', params })
|
return request({ url, method: 'DELETE', params });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ const url = {
|
|||||||
list: (sessionId) => `/session/${sessionId}/message`,
|
list: (sessionId) => `/session/${sessionId}/message`,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 用户
|
||||||
|
user: {
|
||||||
|
login: '/v1/user/login',
|
||||||
|
},
|
||||||
|
|
||||||
// SSE 事件流
|
// SSE 事件流
|
||||||
event: '/event',
|
event: '/event',
|
||||||
};
|
};
|
||||||
|
|||||||
55
src/renderer/stores/spark.js
Normal file
55
src/renderer/stores/spark.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'selected_spark_device';
|
||||||
|
|
||||||
|
export const useSparkStore = defineStore('spark', () => {
|
||||||
|
// 运行时发现的设备列表(动态,不持久化)
|
||||||
|
const devices = ref([]);
|
||||||
|
|
||||||
|
// 当前选中的设备,启动时从 localStorage 恢复
|
||||||
|
const selectedDevice = ref(JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null'));
|
||||||
|
|
||||||
|
// 选中设备的完整 URL,优先取 IPv4 地址
|
||||||
|
const selectedDeviceUrl = computed(() => {
|
||||||
|
if (!selectedDevice.value) return null;
|
||||||
|
const { addresses, port, referer } = selectedDevice.value;
|
||||||
|
// 优先找 IPv4(不含冒号的地址)
|
||||||
|
const ipv4 = addresses?.find((a) => !a.includes(':'));
|
||||||
|
// 兜底用 referer.address(mDNS 响应来源 IP)
|
||||||
|
const ip = ipv4 || referer?.address;
|
||||||
|
return ip ? `http://${ip}:${port}` : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新设备列表(由 BonjourView 调用)
|
||||||
|
function setDevices(list) {
|
||||||
|
devices.value = list;
|
||||||
|
|
||||||
|
// 如果之前选中的设备还在列表里,用最新数据刷新它
|
||||||
|
if (selectedDevice.value) {
|
||||||
|
const fresh = list.find((d) => d.name === selectedDevice.value.name);
|
||||||
|
if (fresh) selectDevice(fresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中某台设备,并持久化到 localStorage
|
||||||
|
function selectDevice(device) {
|
||||||
|
selectedDevice.value = device;
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(device));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除选中
|
||||||
|
function clearSelectedDevice() {
|
||||||
|
selectedDevice.value = null;
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
devices,
|
||||||
|
selectedDevice,
|
||||||
|
selectedDeviceUrl,
|
||||||
|
setDevices,
|
||||||
|
selectDevice,
|
||||||
|
clearSelectedDevice,
|
||||||
|
};
|
||||||
|
});
|
||||||
25
src/renderer/utils/crypto.js
Normal file
25
src/renderer/utils/crypto.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Base64 } from 'js-base64';
|
||||||
|
import JSEncrypt from 'jsencrypt';
|
||||||
|
|
||||||
|
// RSA 公钥,替换为实际公钥内容
|
||||||
|
const RSA_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/
|
||||||
|
z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp
|
||||||
|
2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOO
|
||||||
|
UEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVK
|
||||||
|
RNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK
|
||||||
|
6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs
|
||||||
|
2wIDAQAB
|
||||||
|
-----END PUBLIC KEY-----`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对密码进行加密:Base64 编码 → RSA 加密
|
||||||
|
* @param {string} password 原始密码
|
||||||
|
* @returns {string|false} 加密后的密文,失败返回 false
|
||||||
|
*/
|
||||||
|
export function encryptPassword(password) {
|
||||||
|
const base64Pwd = Base64.encode(password);
|
||||||
|
const encrypt = new JSEncrypt();
|
||||||
|
encrypt.setPublicKey(RSA_PUBLIC_KEY);
|
||||||
|
return encrypt.encrypt(base64Pwd);
|
||||||
|
}
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
import { Search, Refresh } from '@element-plus/icons-vue';
|
import { Search, Refresh } from '@element-plus/icons-vue';
|
||||||
|
import { useSparkStore } from '@/stores/spark';
|
||||||
|
|
||||||
|
const sparkStore = useSparkStore();
|
||||||
const services = ref([]);
|
const services = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
let unsubscribe = null;
|
let unsubscribe = null;
|
||||||
|
|
||||||
|
const filterServices = (list) => list.filter((s) => s.type === 'polygence-spark');
|
||||||
|
|
||||||
const fetchServices = async () => {
|
const fetchServices = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
services.value = await window.bonjour.getServices();
|
const all = await window.bonjour.getServices();
|
||||||
|
const filtered = filterServices(all);
|
||||||
|
services.value = filtered;
|
||||||
|
sparkStore.setDevices(filtered);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch bonjour services:', error);
|
console.error('Failed to fetch bonjour services:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -20,7 +27,9 @@ const fetchServices = async () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchServices();
|
fetchServices();
|
||||||
unsubscribe = window.bonjour.onServicesUpdated((updatedServices) => {
|
unsubscribe = window.bonjour.onServicesUpdated((updatedServices) => {
|
||||||
services.value = updatedServices;
|
const filtered = filterServices(updatedServices);
|
||||||
|
services.value = filtered;
|
||||||
|
sparkStore.setDevices(filtered);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user