diff --git a/package-lock.json b/package-lock.json
index 5b84af3..1773b85 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,8 @@
"bonjour-service": "^1.3.0",
"electron-squirrel-startup": "^1.0.1",
"element-plus": "^2.13.6",
+ "js-base64": "3.7.5",
+ "jsencrypt": "^3.5.4",
"lucide-vue-next": "^1.0.0",
"pinia": "^3.0.4",
"vue": "^3.5.32",
@@ -6919,6 +6921,12 @@
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6939,6 +6947,12 @@
"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": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
diff --git a/package.json b/package.json
index 245ea20..44f7ecc 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,9 @@
"lucide-vue-next": "^1.0.0",
"pinia": "^3.0.4",
"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": {
"*.{js,vue}": [
diff --git a/src/renderer/components/LoginDialog.vue b/src/renderer/components/LoginDialog.vue
index 804b442..9700dc5 100644
--- a/src/renderer/components/LoginDialog.vue
+++ b/src/renderer/components/LoginDialog.vue
@@ -11,6 +11,16 @@
登录后体验更多功能
+
+
+
+
+
@@ -56,6 +66,10 @@
import { ref, watch } from 'vue';
import { User } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
+import { useSparkStore } from '@/stores/spark';
+import { loginAction } from '@/http/api.js';
+
+const sparkStore = useSparkStore();
const props = defineProps({
modelValue: {
@@ -78,11 +92,13 @@ const loading = ref(false);
const agreed = ref(false);
const form = ref({
+ sparkDevice: sparkStore.selectedDevice?.name || '',
username: '',
password: '',
});
const rules = {
+ sparkDevice: [{ required: true, message: '请选择设备', trigger: 'change' }],
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
};
@@ -96,10 +112,16 @@ async function handleLogin() {
if (!valid) return;
loading.value = true;
try {
- // TODO: 替换为真实登录接口
- await new Promise((r) => setTimeout(r, 800));
- ElMessage.success('登录成功');
- emit('login-success', { username: form.value.username });
+ const device = sparkStore.devices.find((d) => d.name === form.value.sparkDevice);
+ if (device) sparkStore.selectDevice(device);
+
+ 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;
} catch (err) {
ElMessage.error('登录失败,请重试');
diff --git a/src/renderer/http/api.js b/src/renderer/http/api.js
index fa3d73b..bbd340e 100644
--- a/src/renderer/http/api.js
+++ b/src/renderer/http/api.js
@@ -1,9 +1,14 @@
import { getAction, postAction, deleteAction } from './manage.js';
import url, { getBaseUrl } from './url.js';
+import { encryptPassword } from '@/utils/crypto.js';
// 健康检查
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 getSessionAction = (id) => getAction(url.session.detail(id));
diff --git a/src/renderer/http/manage.js b/src/renderer/http/manage.js
index 63cbd3d..5d639e7 100644
--- a/src/renderer/http/manage.js
+++ b/src/renderer/http/manage.js
@@ -1,17 +1,17 @@
-import request from './index.js'
+import request from './index.js';
export function getAction(url, params) {
- return request({ url, method: 'GET', params })
+ return request({ url, method: 'GET', params });
}
-export function postAction(url, data, headers = {}) {
- return request({ url, method: 'POST', data, headers })
+export function postAction(url, data, headers = {}, baseURL) {
+ return request({ url, method: 'POST', data, headers, ...(baseURL ? { baseURL } : {}) });
}
export function putAction(url, data) {
- return request({ url, method: 'PUT', data })
+ return request({ url, method: 'PUT', data });
}
export function deleteAction(url, params) {
- return request({ url, method: 'DELETE', params })
+ return request({ url, method: 'DELETE', params });
}
diff --git a/src/renderer/http/url.js b/src/renderer/http/url.js
index d3b6b74..476cf4e 100644
--- a/src/renderer/http/url.js
+++ b/src/renderer/http/url.js
@@ -22,6 +22,11 @@ const url = {
list: (sessionId) => `/session/${sessionId}/message`,
},
+ // 用户
+ user: {
+ login: '/v1/user/login',
+ },
+
// SSE 事件流
event: '/event',
};
diff --git a/src/renderer/stores/spark.js b/src/renderer/stores/spark.js
new file mode 100644
index 0000000..ebad1c5
--- /dev/null
+++ b/src/renderer/stores/spark.js
@@ -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,
+ };
+});
diff --git a/src/renderer/utils/crypto.js b/src/renderer/utils/crypto.js
new file mode 100644
index 0000000..3cacd86
--- /dev/null
+++ b/src/renderer/utils/crypto.js
@@ -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);
+}
diff --git a/src/renderer/views/bonjour/BonjourView.vue b/src/renderer/views/bonjour/BonjourView.vue
index 244c283..14f1d6e 100644
--- a/src/renderer/views/bonjour/BonjourView.vue
+++ b/src/renderer/views/bonjour/BonjourView.vue
@@ -1,15 +1,22 @@