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 @@