feat/初始化

This commit is contained in:
季万俊 2025-08-26 15:06:19 +08:00
parent d402af23f4
commit 3c84959842
14 changed files with 3394 additions and 0 deletions

33
package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "my-vue-app",
"private": true,
"version": "0.2.0",
"scripts": {
"dev": "vite",
"build": "vite build --mode production",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@types/dat.gui": "^0.7.13",
"axios": "^1.6.7",
"dat.gui": "^0.7.9",
"echarts": "^6.0.0",
"element-plus": "^2.10.2",
"less": "^4.3.0",
"moment": "^2.30.1",
"mqtt": "^5.13.2",
"sass": "^1.89.2",
"sass-loader": "^16.0.5",
"vue": "^3.2.25",
"vue-router": "^4.5.1",
"wav-encoder": "^1.3.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.4",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.7.0",
"vite": "^2.9.15"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

16
src/App.vue Normal file
View File

@ -0,0 +1,16 @@
<!--
* @Author: 季万俊
* @Date: 2025-06-10 14:36:16
* @Description:
-->
<template>
<router-view></router-view>
</template>
<script setup>
import { onMounted } from "vue";
onMounted(() => {
});
</script>

90
src/config/index.js Normal file
View File

@ -0,0 +1,90 @@
/*
* @Author: 季万俊
* @Date: 2025-08-22 17:04:41
* @Description:
*/
const config = {
"page": "Ecommerce Home",
"commands": [
{
"command": "open_login_111",
"description": "打开登录弹窗或页面",
"action": "click",
"selector": "#login-button",
"params": []
},
{
"command": "open_login_222_333",
"description": "打开登录弹窗或页面",
"action": "click",
"selector": "#login-button",
"params": []
},
{
"command": "input_username",
"description": "在用户名输入框中填写用户名",
"action": "input",
"selector": "#username-input",
"params": [
{
"name": "username",
"type": "string",
"description": "要输入的用户名"
}
]
},
{
"command": "input_password",
"description": "在密码输入框中填写密码",
"action": "input",
"selector": "#password-input",
"params": [
{
"name": "password",
"type": "string",
"description": "要输入的密码"
}
]
},
{
"command": "submit_login",
"description": "提交登录表单",
"action": "click",
"selector": "#submit-login",
"params": []
},
{
"command": "navigate_to_product",
"description": "导航到指定产品页面",
"action": "navigate",
"selector": "",
"params": [
{
"name": "product_url",
"type": "string",
"description": "产品页面的URL"
}
]
},
{
"command": "add_to_cart",
"description": "将当前产品添加到购物车",
"action": "click",
"selector": ".add-to-cart-btn",
"params": []
},
{
"command": "search_product",
"description": "在搜索框中输入关键词并搜索",
"action": "input_and_submit",
"selector": "#search-input",
"params": [
{
"name": "keyword",
"type": "string",
"description": "搜索关键词"
}
]
}
]
};

18
src/main.js Normal file
View File

@ -0,0 +1,18 @@
/*
* @Author: 季万俊
* @Date: 2025-06-10 14:36:16
* @Description:
*/
import { createApp } from 'vue'
import App from './App.vue'
import 'element-plus/dist/index.css'
import ElementPlus from 'element-plus'
import zhCN from 'element-plus/dist/locale/zh-cn.mjs' // 引入中文
import router from './router' // 引入路由配置
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(ElementPlus, { locale: zhCN }).use(router).mount('#app')

24
src/router/index.js Normal file
View File

@ -0,0 +1,24 @@
/*
* @Author: 季万俊
* @Date: 2025-08-07 10:45:45
* @Description:
*/
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 导入需要路由的组件
import index from './../view/index.vue';
const routes = [
{
path: '/',
name: 'index',
component: index
},
]
const router = createRouter({
history: createWebHistory('/'),
routes
})
export default router

604
src/util/client.js Normal file
View File

@ -0,0 +1,604 @@
import mqtt from "mqtt";
// 实例池用于缓存相同配置的实例仅用于MQTT
const instancePool = new Map();
class DataMiddleware {
/**
* 构造函数
* @param {Object} options - 实例配置
*/
constructor(options = {}) {
// 存储实例配置
this.options = {
url: options.url || '',
reconnectConfig: {
maxRetries: 10, //最大重连次数
initialDelay: 1000, //
maxDelay: 30000,
...options.reconnectConfig
},
mqttOptions: options.mqttOptions || {},
httpOptions: options.httpOptions || {},
reconnect: true
};
// 存储所有请求/订阅的信息
this.requests = {
http: new Map(), // HTTP请求仅用于跟踪当前请求以便取消
mqtt: new Map() // MQTT订阅缓存: key为topic
};
// 客户端实例
this.mqttClient = null;
// 连接状态
this.connectionStatus = {
mqtt: 'disconnected', // disconnected, connecting, connected, error
http: 'idle',
retryCount: 0
};
// 用于生成唯一订阅ID
this.subscriptionIdCounter = 0;
}
/**
* 获取实例的唯一标识键
*/
getInstanceKey() {
return this.options.url;
}
/**
* 静态方法获取或创建实例
*/
static getInstance(options = {}) {
const key = options.url || '';
if (instancePool.has(key)) {
return instancePool.get(key);
}
const instance = new DataMiddleware(options);
instancePool.set(key, instance);
return instance;
}
/**
* 获取当前连接状态
*/
getStatus() {
return { ...this.connectionStatus };
}
/**
* 获取当前MQTT客户端实例用于发布消息
*/
getMqttClient() {
return this.mqttClient;
}
/**
* HTTP请求方法 - 优化参数处理支持keys可选
* @param {string} url - 请求地址
* @param {string[]|Function} [keys] - 需要提取的key数组可选
* @param {Function} [callback] - 回调函数格式: (data, error) => {}可选
* @returns {Promise} 返回Promise对象同时通过callback暴露结果
*/
httpRequest(url, keys, callback) {
// 处理参数支持keys可选自动识别callback
let actualKeys = [];
let actualCallback = null;
if (typeof keys === 'function') {
actualCallback = keys;
actualKeys = [];
} else if (Array.isArray(keys) && typeof callback === 'function') {
actualKeys = keys;
actualCallback = callback;
} else if (Array.isArray(keys)) {
actualKeys = keys;
actualCallback = null;
} else if (arguments.length === 1) {
actualKeys = [];
actualCallback = null;
}
// 生成唯一请求ID
const requestId = this.generateSubscriptionId();
// 创建AbortController用于取消请求
const controller = new AbortController();
// 存储请求信息以便后续取消
this.requests.http.set(requestId, {
controller,
url
});
// 将keys数组转换为逗号分隔的字符串
const keysStr = Array.isArray(actualKeys) && actualKeys.length > 0
? actualKeys.join(',')
: '';
// 构建URL并添加keys参数
const fullUrl = new URL(url);
if (keysStr) {
fullUrl.searchParams.append('keys', keysStr);
}
// 构建请求选项,禁用缓存
const fetchOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
...this.options.httpOptions.headers
},
signal: controller.signal,
...this.options.httpOptions
};
this.connectionStatus.http = 'loading';
// 返回Promise同时支持callback
const promise = new Promise((resolve, reject) => {
fetch(fullUrl.toString(), fetchOptions)
.then(response => {
this.connectionStatus.http = 'idle';
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
// 请求完成后从跟踪表中移除
this.requests.http.delete(requestId);
// 根据keys过滤数据
// const filteredData = this._filterDataByKeys(data, actualKeys);
// 调用回调函数(如果提供)
if (typeof actualCallback === 'function') {
actualCallback(data, null);
}
resolve(data);
})
.catch(error => {
this.connectionStatus.http = 'idle';
this.requests.http.delete(requestId);
// 忽略取消请求的错误
if (error.name !== 'AbortError') {
// 调用回调函数(如果提供)
if (typeof actualCallback === 'function') {
actualCallback(null, error);
}
reject(error);
}
});
});
// 将请求ID附加到promise上方便取消请求
promise.requestId = requestId;
return promise;
}
/**
* 取消HTTP请求
* @param {string} requestId - 请求ID
* @returns {boolean} 是否取消成功
*/
cancelHttpRequest(requestId) {
if (this.requests.http.has(requestId)) {
const { controller } = this.requests.http.get(requestId);
controller.abort();
this.requests.http.delete(requestId);
return true;
}
return false;
}
/**
* 连接到MQTT服务器
*/
_connectMqtt() {
if (!this.options.url) {
console.error('MQTT WebSocket URL未配置');
return;
}
// 如果已有连接,先断开
if (this.mqttClient) {
this.mqttClient.end(false, {}, () => {
console.log('已断开现有MQTT连接');
});
}
this.connectionStatus.mqtt = 'connecting';
this._notifyAllMqttSubscribers({
type: 'status',
status: 'connecting'
});
// 使用mqtt.js连接
this.mqttClient = mqtt.connect(this.options.url, {
clientId: `vue-client-${Math.random().toString(36).substr(2, 10)}`,
clean: true,
reconnectPeriod: 0, // 禁用内置重连,使用自定义重连策略
...this.options.mqttOptions
});
// 连接成功回调
this.mqttClient.on('connect', () => {
console.log('MQTT连接成功');
this.connectionStatus.mqtt = 'connected';
this.connectionStatus.retryCount = 0;
// 通知所有订阅者
this._notifyAllMqttSubscribers({
type: 'status',
status: 'connected'
});
// 重新订阅所有主题
this.requests.mqtt.forEach((info, topic) => {
this.mqttClient.subscribe(topic, { qos: info.qos });
});
});
// 收到消息回调
this.mqttClient.on('message', (topic, message) => {
try {
const data = JSON.parse(message.toString());
this._onMqttMessage(topic, data);
} catch (error) {
console.error('解析MQTT消息失败:', error);
this._notifyMqttError(topic, new Error('消息格式错误'));
}
});
// 连接断开回调
this.mqttClient.on('close', () => {
if (this.connectionStatus.mqtt !== 'disconnected') {
console.log('MQTT连接已关闭');
this.connectionStatus.mqtt = 'disconnected';
this._notifyAllMqttSubscribers({
type: 'status',
status: 'disconnected'
});
if(this.reconnect) {
this._scheduleReconnect();
}
}
});
// 错误回调
this.mqttClient.on('error', (error) => {
console.error('MQTT错误:', error);
this.connectionStatus.mqtt = 'error';
this._notifyAllMqttSubscribers({
type: 'error',
error: new Error(`连接错误: ${error.message}`)
});
});
}
/**
* 订阅MQTT主题
*/
mqttSubscribe(topics, callback, options = {}) {
if (!this.options.url) {
throw new Error('MQTT WebSocket URL未配置');
}
// 验证输入参数
if (!Array.isArray(topics) || topics.length === 0) {
throw new Error('请提供有效的主题数组');
}
if (typeof callback !== 'function') {
throw new Error('请提供回调函数');
}
const subIds = [];
// 遍历主题数组,为每个主题创建订阅
topics.forEach(topic => {
const subId = this.generateSubscriptionId();
const mqttKey = topic;
// 添加订阅者
if (this.requests.mqtt.has(mqttKey)) {
const mqttInfo = this.requests.mqtt.get(mqttKey);
mqttInfo.subscribers[subId] = { callback };
} else {
const mqttInfo = {
topic,
options,
subscribers: {
[subId]: { callback }
},
qos: options.qos || 1
};
this.requests.mqtt.set(mqttKey, mqttInfo);
}
subIds.push(subId);
});
// 连接MQTT如果尚未连接
if (!this.mqttClient || !this.mqttClient.connected) {
this._connectMqtt();
} else {
// 已连接状态下直接订阅所有主题
topics.forEach(topic => {
this.mqttClient.subscribe(topic, { qos: options.qos || 0 });
});
}
// 返回所有订阅ID的数组
return subIds;
}
/**
* 处理接收到的MQTT消息
*/
_onMqttMessage(topic, data) {
const mqttInfo = this.requests.mqtt.get(topic);
if (!mqttInfo) return;
// 通知该主题的所有订阅者
Object.values(mqttInfo.subscribers).forEach(({ keys, callback }) => {
try {
const filteredData = this._filterDataByKeys(data, keys);
callback({
type: 'data',
data: filteredData,
topic,
timestamp: Date.now()
}, null);
} catch (error) {
console.error('通知MQTT订阅者失败:', error);
}
});
}
/**
* 发布MQTT消息
*/
mqttPublish(topic, message, options = {}) {
return new Promise((resolve, reject) => {
if (!this.mqttClient || !this.mqttClient.connected) {
reject(new Error('MQTT客户端未连接'));
return;
}
this.mqttClient.publish(topic, JSON.stringify(message), options, (error) => {
if (error) {
reject(error);
} else {
resolve(true);
}
});
});
}
/**
* 安排重连
*/
_scheduleReconnect() {
if (this.connectionStatus.retryCount >= this.options.reconnectConfig.maxRetries) {
console.error('已达到最大重连次数');
return;
}
this.connectionStatus.retryCount++;
const delay = Math.min(
this.options.reconnectConfig.initialDelay * Math.pow(2, this.connectionStatus.retryCount - 1),
this.options.reconnectConfig.maxDelay
);
console.log(`将在 ${delay}ms 后尝试重连 (第 ${this.connectionStatus.retryCount} 次)`);
setTimeout(() => {
this._connectMqtt();
}, delay);
}
/**
* 计算重连延迟
*/
_calculateReconnectDelay(attempt) {
const delay = this.options.reconnectConfig.initialDelay * Math.pow(2, attempt);
return Math.min(delay, this.options.reconnectConfig.maxDelay);
}
/**
* 取消订阅
*/
unsubscribe(data) {
if(data && data.length) {
data.forEach((d)=> {
this.unsubscribeFun(d);
})
}
}
/**
* 取消订阅
*/
unsubscribeFun(subId) {
// if (!['http', 'mqtt'].includes(type)) {
// console.error('无效的订阅类型');
// return false;
// }
let removed = false;
this.requests['mqtt'].forEach((info, key) => {
if (info.subscribers && info.subscribers[subId]) {
delete info.subscribers[subId];
removed = true;
console.log(info.subscribers,"----info.subscribers");
// 如果没有订阅者了,清理资源
if (info.subscribers && Object.keys(info.subscribers).length === 0) {
console.log('移除------');
this._cleanupRequest( key, info);
}
}
});
return removed;
}
/**
* 清理不再有订阅者的请求/订阅
*/
_cleanupRequest(key, info) {
if (this.mqttClient && this.mqttClient.connected) {
this.mqttClient.unsubscribe(info.topic);
}
this.requests.mqtt.delete(key);
if(!this.requests.mqtt.size) {
this.destroy();
}
// if (type === 'http') {
// 清除HTTP请求的控制器
// if (info.controller) {
// info.controller.abort();
// }
// this.requests.http.delete(key);
// } else if (type === 'mqtt') {
// if (this.mqttClient && this.mqttClient.connected) {
// this.mqttClient.unsubscribe(info.topic);
// }
// this.requests.mqtt.delete(key);
// }
}
/**
* 生成唯一订阅ID
*/
generateSubscriptionId() {
return `sub_${this.subscriptionIdCounter++}`;
}
/**
* 根据key数组过滤数据
*/
_filterDataByKeys(data, keys) {
if (!keys || keys.length === 0) {
return data;
}
if (Array.isArray(data)) {
return data.map(item => this._filterObjectByKeys(item, keys));
}
return this._filterObjectByKeys(data, keys);
}
/**
* 根据key数组过滤对象属性
*/
_filterObjectByKeys(obj, keys) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
return keys.reduce((result, key) => {
const value = this._getNestedValue(obj, key);
if (value !== undefined) {
result[key] = value;
}
return result;
}, {});
}
/**
* 获取对象的嵌套属性值
*/
_getNestedValue(obj, key) {
return key.split('.').reduce((current, part) => {
if (current && typeof current === 'object' && part in current) {
return current[part];
}
return undefined;
}, obj);
}
/**
* 通知所有MQTT订阅者
*/
_notifyAllMqttSubscribers(message) {
this.requests.mqtt.forEach((mqttInfo) => {
Object.values(mqttInfo.subscribers).forEach(({ callback }) => {
try {
if (message.error) {
callback(null, message.error);
} else {
callback(message, null);
}
} catch (err) {
console.error('通知MQTT订阅者失败:', err);
}
});
});
}
/**
* 通知MQTT订阅者错误
*/
_notifyMqttError(topic, error) {
const mqttInfo = this.requests.mqtt.get(topic);
if (!mqttInfo) return;
Object.values(mqttInfo.subscribers).forEach(({ callback }) => {
try {
callback(null, error);
} catch (err) {
console.error('通知MQTT订阅者错误失败:', err);
}
});
}
/**
* 销毁实例
*/
destroy() {
// 断开MQTT连接
if (this.mqttClient) {
this.reconnect = false;
this.mqttClient.end(true);
this.mqttClient = null;
}
// 取消所有HTTP请求
// this.requests.http.forEach(info => {
// if (info.controller) {
// info.controller.abort();
// }
// });
// 清空请求缓存
// this.requests.http.clear();
this.requests.mqtt.clear();
// 从实例池移除
instancePool.delete(this.getInstanceKey());
}
}
export default DataMiddleware;

55
src/util/index.js Normal file
View File

@ -0,0 +1,55 @@
// 防抖函数实现
export const debounce = (func, delay) => {
let timer = null;
// 返回一个新函数
return function (...args) {
// 保存当前上下文
const context = this;
// 如果定时器已存在,则清除它
if (timer) {
clearTimeout(timer);
}
// 创建新的定时器
timer = setTimeout(() => {
// 延迟后执行原函数,并绑定正确的上下文和参数
func.apply(context, args);
// 执行后清除定时器
timer = null;
}, delay);
};
}
// 节流函数实现 - 定时器版(确保最后一次调用会被执行)
export const throttle = (func, interval) => {
console.log(interval);
let timer = null;
let lastTime = 0;
return function(...args) {
const context = this;
const now = Date.now();
// 清除之前的定时器
if (timer) {
clearTimeout(timer);
timer = null;
}
// 如果达到执行间隔,立即执行
if (now - lastTime >= interval) {
func.apply(context, args);
lastTime = now;
} else {
// 否则设置定时器,确保最后一次调用会被执行
timer = setTimeout(() => {
func.apply(context, args);
lastTime = Date.now();
timer = null;
}, interval - (now - lastTime));
}
};
}

451
src/util/voice-system.js Normal file
View File

@ -0,0 +1,451 @@
class VoiceControlSystem {
constructor(config) {
this.config = config;
this.isListening = false;
this.recognition = null;
this.apiKey = 'sk-020189889aac40f3b050f7c60ca597f8'; // 替换为您的DeepSeek API密钥
this.init();
}
init() {
console.log(123);
this.setupSpeechRecognition();
this.setupEventListeners();
this.uploadConfigToModel();
this.getSupportedLanguages();
}
// 检测浏览器支持的语音识别语言(部分 Chromium 浏览器支持)
async getSupportedLanguages() {
console.log(123);
if (window.SpeechRecognition?.getSupportedLanguages) {
try {
const languages = await window.SpeechRecognition.getSupportedLanguages();
console.log('浏览器支持的语言列表:', languages);
return languages;
} catch (e) {
console.warn('无法获取支持的语言列表:', e);
}
}
// 若浏览器不支持 getSupportedLanguages返回常见兼容语言
return ['zh-CN', 'en-US', 'zh-TW', 'en-GB'];
}
setupSpeechRecognition() {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
this.updateStatus('浏览器不支持语音识别功能', 'error');
return;
}
this.recognition = new SpeechRecognition();
this.recognition.lang = 'zh-CN';
this.recognition.extra = {
'alternatives': 5,
'language': ['zh-CN', 'zh-HK', 'zh-TW', 'en-US'] // 提供备选语言
};
this.recognition.continuous = false;
this.recognition.interimResults = false;
this.recognition.onstart = () => {
this.isListening = true;
this.updateStatus('正在聆听...', 'listening');
document.getElementById('voice-btn').classList.add('listening');
};
this.recognition.onend = () => {
this.isListening = false;
document.getElementById('voice-btn').classList.remove('listening');
this.updateStatus('准备就绪', 'ready');
};
this.recognition.onresult = async (event) => {
const transcript = event.results[0][0].transcript;
console.log(transcript,"-----transcript");
this.updateStatus(`识别结果: "${transcript}"`, 'success');
await this.processVoiceCommand(transcript);
};
this.recognition.onerror = (event) => {
this.updateStatus(`语音识别错误: ${event.error}`, 'error');
};
}
setupEventListeners() {
const voiceBtn = document.getElementById('voice-btn');
voiceBtn.addEventListener('click', () => {
const input = document.getElementById('input-text');
let val = input.value;
console.log(val,"---val");
this.toggleListening(val)
});
// 添加API密钥配置界面
this.setupApiKeyConfig();
}
setupApiKeyConfig() {
const configBtn = document.createElement('button');
configBtn.textContent = '配置API密钥';
configBtn.style.marginLeft = '10px';
configBtn.addEventListener('click', () => this.showApiKeyConfig());
document.getElementById('voice-btn').after(configBtn);
}
showApiKeyConfig() {
const apiKey = prompt('请输入DeepSeek API密钥:', this.apiKey);
if (apiKey !== null) {
this.apiKey = apiKey;
this.updateStatus('API密钥已更新', 'success');
}
}
async toggleListening(value) {
if (!this.apiKey) {
this.updateStatus('请先配置DeepSeek API密钥', 'error');
this.showApiKeyConfig();
return;
}
let transcript = value;
await this.processVoiceCommand(transcript);
// if (this.isListening) {
// this.recognition.stop();
// } else {
// this.recognition.start();
// }
}
async uploadConfigToModel() {
try {
this.updateStatus('配置文件已加载', 'info');
console.log('DeepSeek配置已准备:', this.config);
} catch (error) {
this.updateStatus('配置加载失败', 'error');
}
}
async processVoiceCommand(voiceText) {
try {
this.updateStatus('正在调用DeepSeek分析指令...', 'info');
const sequence = await this.callDeepSeekAPI(voiceText);
this.updateStatus('DeepSeek指令序列生成成功', 'success');
await this.executeSequence(sequence);
} catch (error) {
console.error('DeepSeek处理错误:', error);
this.updateStatus(`处理失败: ${error.message}`, 'error');
// 尝试使用备用方案
try {
this.updateStatus('尝试使用备用方案...', 'info');
const fallbackSequence = this.fallbackModelResponse(voiceText);
await this.executeSequence(fallbackSequence);
} catch (fallbackError) {
this.updateStatus('备用方案也失败了', 'error');
}
}
}
async callDeepSeekAPI(voiceText) {
const API_URL = 'https://api.deepseek.com/v1/chat/completions';
if (!this.apiKey) {
throw new Error('请先配置DeepSeek API密钥');
}
const prompt = this.buildDeepSeekPrompt(voiceText);
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
model: "deepseek-chat",
messages: [
{
role: "system",
content: `你是一个专业的网页语音控制助手。请严格根据提供的配置文件和用户指令,生成准确的操作序列。
重要规则
1. 只返回纯JSON格式不要包含任何其他文本
2. JSON结构必须包含sequence数组
3. 每个指令必须存在于配置文件中
4. 参数必须匹配指令定义
5. 按逻辑顺序排列指令`
},
{
role: "user",
content: prompt
}
],
temperature: 0.1,
max_tokens: 1000,
response_format: { type: "json_object" }
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`DeepSeek API错误: ${response.status} ${errorData.message || ''}`);
}
const data = await response.json();
const content = data.choices[0].message.content;
// 解析JSON响应
let result;
try {
result = JSON.parse(content);
} catch (parseError) {
// 尝试提取JSON内容
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
result = JSON.parse(jsonMatch[0]);
} else {
throw new Error('DeepSeek返回了非JSON格式的响应');
}
}
console.log(result,"---result");
// 验证响应结构
if (!result.sequence || !Array.isArray(result.sequence)) {
throw new Error('DeepSeek返回了无效的指令序列格式');
}
return result;
} catch (error) {
console.error('DeepSeek API调用错误:', error);
throw new Error(`DeepSeek处理失败: ${error.message}`);
}
}
buildDeepSeekPrompt(voiceText) {
// 构建指令选择指南
const commandSelectionGuide = this.buildCommandSelectionGuide();
return `网页交互配置文件:
${JSON.stringify(this.config, null, 2)}
用户语音指令"${voiceText}"
${commandSelectionGuide}
请严格遵循以下规则生成指令序列
1. 每个用户指令只选择最匹配的一个命令
2. 不要重复执行相同功能的命令
3. 如果多个命令描述相似选择命令名称最简洁的那个
4. 确保指令顺序合理
5. 只返回纯JSON格式
要求格式
{
"sequence": [
{"command": "指令名", "params": {参数对象}}
]
}
请为指令"${voiceText}"生成正确的序列`;
}
buildCommandSelectionGuide() {
// 分组相似命令
const commandGroups = {};
this.config.commands.forEach(cmd => {
const key = cmd.description + cmd.selector;
if (!commandGroups[key]) {
commandGroups[key] = [];
}
commandGroups[key].push(cmd);
});
let guide = "指令选择指南:\n";
Object.values(commandGroups).forEach(group => {
if (group.length > 1) {
guide += `\n相似命令组(选择其中一个):\n`;
group.forEach(cmd => {
guide += `- ${cmd.command}: ${cmd.description}\n`;
});
// 推荐选择规则
const recommended = group.reduce((prev, current) =>
prev.command.length < current.command.length ? prev : current
);
guide += `推荐选择: ${recommended.command} (名称最简洁)\n`;
}
});
return guide;
}
// 备用方案当DeepSeek API不可用时
fallbackModelResponse(voiceText) {
const lowerText = voiceText.toLowerCase();
// 简单的规则匹配
if (lowerText.includes('登录')) {
const username = this.extractParam(voiceText, '用户') || this.extractParam(voiceText, '账号') || 'testuser';
const password = this.extractParam(voiceText, '密码') || '123456';
return {
sequence: [
{ command: "open_login" },
{ command: "input_username", params: { username } },
{ command: "input_password", params: { password } },
{ command: "submit_login" }
]
};
}
if (lowerText.includes('搜索')) {
const keyword = this.extractSearchKeyword(voiceText) || '商品';
return {
sequence: [
{ command: "search_product", params: { keyword } }
]
};
}
if (lowerText.includes('购物车') || lowerText.includes('加入')) {
return {
sequence: [
{ command: "add_to_cart" }
]
};
}
throw new Error('无法识别的指令');
}
extractParam(text, paramName) {
const regex = new RegExp(`${paramName}[:]*\\s*([^\\s]+)`, 'i');
const match = text.match(regex);
return match ? match[1] : null;
}
extractSearchKeyword(text) {
const regex = /搜索[:]*\s*([^。,!?]+)/i;
const match = text.match(regex);
return match ? match[1].trim() : null;
}
async executeSequence(sequence) {
for (const [index, instruction] of sequence.sequence.entries()) {
try {
await this.executeInstruction(instruction);
this.logCommand(`✓ 完成: ${instruction.command}`);
// 在指令之间添加延迟
if (index < sequence.sequence.length - 1) {
await this.delay(800);
}
} catch (error) {
this.logCommand(`✗ 错误: ${instruction.command} - ${error.message}`);
throw error;
}
}
}
async executeInstruction(instruction) {
const commandConfig = this.config.commands.find(c => c.command === instruction.command);
if (!commandConfig) {
throw new Error(`未知指令: ${instruction.command}`);
}
this.logCommand(`开始执行: ${instruction.command}`);
const element = document.querySelector(commandConfig.selector);
if (!element) {
throw new Error(`找不到元素: ${commandConfig.selector}`);
}
// 滚动到元素可见
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
switch (commandConfig.action) {
case 'click':
element.click();
this.logCommand(`点击: ${commandConfig.selector}`);
break;
case 'input':
const inputParam = commandConfig.params[0];
if (instruction.params && instruction.params[inputParam.name]) {
element.value = instruction.params[inputParam.name];
element.dispatchEvent(new Event('input', { bubbles: true }));
this.logCommand(`输入${inputParam.name}: ${instruction.params[inputParam.name]}`);
}
break;
case 'navigate':
if (instruction.params && instruction.params.product_url) {
window.location.href = instruction.params.product_url;
}
break;
case 'input_and_submit':
if (instruction.params && instruction.params.keyword) {
element.value = instruction.params.keyword;
element.dispatchEvent(new Event('input', { bubbles: true }));
// 尝试提交表单或点击相关按钮
if (element.form) {
element.form.submit();
} else {
// 查找提交按钮
const submitBtn = document.querySelector('#search-submit, [type="submit"]');
if (submitBtn) submitBtn.click();
}
this.logCommand(`搜索: ${instruction.params.keyword}`);
}
break;
default:
throw new Error(`未知动作类型: ${commandConfig.action}`);
}
// 添加视觉反馈
this.highlightElement(element);
}
highlightElement(element) {
const originalStyle = element.style.boxShadow;
element.style.boxShadow = '0 0 0 3px #4CAF50';
setTimeout(() => {
element.style.boxShadow = originalStyle;
}, 1000);
}
updateStatus(message, type = 'info') {
const statusElement = document.getElementById('status');
statusElement.textContent = message;
statusElement.className = `status ${type}`;
}
logCommand(message) {
return
const list = document.getElementById('command-list');
const item = document.createElement('li');
item.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
list.appendChild(item);
list.scrollTop = list.scrollHeight;
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 初始化系统
document.addEventListener('DOMContentLoaded', () => {
new VoiceControlSystem(config);
});

View File

@ -0,0 +1,684 @@
<!--
* @Author: 季万俊
* @Date: 2025-08-22 17:03:18
* @Description:
-->
<template>
<div class="container">
<!-- 固定在左下角的语音控制组件 -->
<div class="voice-control-container">
<div class="status-text">点击开始语音识别</div>
<button class="voice-btn" id="voice-btn" @click="toggleListening">
<svg
style="
position: absolute;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
"
t="1756171270753"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="14442"
width="42"
height="70"
>
<path
d="M704 192v368c0 52.8-21.6 100.8-56.4 135.6S564.8 752 512 752c-105.6 0-192-86.4-192-192V192C320 86.4 406.4 0 512 0s192 86.4 192 192z"
p-id="14443"
data-spm-anchor-id="a313x.search_index.0.i2.72cc3a81bxN4ca"
class="selected"
fill="#ffffff"
></path>
<path
d="M816 496v144c0 2.8-0.4 5.6-1.1 8.4-18.5 68.2-58.9 126.1-112.3 166.9-43.5 33.2-95.6 55.2-151.6 62.2-4 0.5-7 3.9-7 7.9V944c0 8.8 7.2 16 16 16h80c35.3 0 64 28.7 64 64H320c0-35.3 28.7-64 64-64h80c8.8 0 16-7.2 16-16v-58.5c0-4-3-7.4-7-7.9-124.8-15.7-230.3-105.5-263.9-229.2-0.7-2.7-1.1-5.6-1.1-8.4V496.7c0-17.4 13.7-32.2 31.1-32.7 18.1-0.5 32.9 14 32.9 32v129.8c0 6.9 1.1 13.8 3.3 20.3C309.3 746.9 404.6 816 512 816s202.7-69.1 236.7-169.9c2.2-6.5 3.3-13.4 3.3-20.3V496.7c0-17.4 13.7-32.2 31.1-32.7 18.1-0.5 32.9 14 32.9 32z"
p-id="14444"
data-spm-anchor-id="a313x.search_index.0.i3.72cc3a81bxN4ca"
class="selected"
fill="#ffffff"
></path>
</svg>
<div class="pulse-ring"></div>
</button>
<div class="command-display">
<div class="command-text"></div>
<div class="command-action" v-for="item in action">
{{
"执行操作:" +
`${item.command} ${
item.params && item.params.keyword
? "-" + item.params.keyword
: ""
}`
}}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
const props = defineProps({
config: {
type: Object,
default: () => {},
},
});
watch(config, (newVal, oldVal) => {
if (voiceControl.value && voiceControl.value.isListening) {
voiceControl.value.stopListening();
}
initVoiceControl();
});
const config = {
page: "Ecommerce Home",
commands: [
{
command: "open_login",
description: "打开登录弹窗或页面",
action: "click",
selector: "#login-button, .login-btn, [data-login]",
params: [],
},
{
command: "input_username",
description: "在用户名输入框中填写用户名",
action: "input",
selector: "#username, #username-input, input[type='text'][name*='user']",
params: [
{
name: "username",
type: "string",
description: "要输入的用户名",
},
],
},
{
command: "input_password",
description: "在密码输入框中填写密码",
action: "input",
selector: "#password, #password-input, input[type='password']",
params: [
{
name: "password",
type: "string",
description: "要输入的密码",
},
],
},
{
command: "submit_login",
description: "提交登录表单",
action: "click",
selector: "#submit-login, .login-submit, button[type='submit']",
params: [],
},
{
command: "search_product",
description: "在搜索框中输入关键词",
action: "input",
selector: "#search-input, .search-box, input[type='search']",
params: [
{
name: "keyword",
type: "string",
description: "搜索关键词",
},
],
},
{
command: "perform_search",
description: "执行搜索操作",
action: "click",
selector: "#search-button, .search-btn, button[type='submit']",
params: [],
},
{
command: "add_to_cart",
description: "将当前产品添加到购物车",
action: "click",
selector: ".add-to-cart, .cart-btn, [data-add-to-cart]",
params: [],
},
],
};
class VoiceControl {
constructor(callback) {
this.config = config;
this.isListening = false;
this.apiKey = "sk-020189889aac40f3b050f7c60ca597f8";
this.setupSpeechRecognition();
this.updateUI();
this.callback = callback;
}
setupSpeechRecognition() {
const SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
this.showError("您的浏览器不支持语音识别API");
return;
}
this.recognition = new SpeechRecognition();
//
this.recognition.continuous = true; //
this.recognition.interimResults = true; //
this.recognition.maxAlternatives = 1; //
this.recognition.lang = "zh-CN"; //
//
this.recognition.energy_threshold = 300; //
this.recognition.pause_threshold = 0.3; //
this.recognition.phrase_threshold = 0.2; //
this.lastFinalTranscript = ""; //
this.isProcessing = false; //
this.recognition.onstart = () => {
this.isListening = true;
this.updateUI();
this.showStatus("正在聆听...", "info");
document.querySelector(".status-text").textContent = "正在聆听...";
};
this.recognition.onresult = async (event) => {
//
if (this.isProcessing) return;
//
const lastResultIndex = event.results.length - 1;
const result = event.results[lastResultIndex];
const transcript = result[0].transcript;
console.log(event, "-------event");
this.showTranscript(transcript);
this.showStatus("正在处理指令...", "info");
//
const commandDisplay = document.querySelector(".command-display");
const commandText = commandDisplay.querySelector(".command-text");
console.log(result.isFinal, "----result.isFinal");
if (!result.isFinal) {
// -
commandText.textContent = `正在识别:${transcript}`;
commandText.style.color = "#00000085";
commandDisplay.classList.add("show");
return; //
}
//
this.isProcessing = true;
if (transcript) {
commandText.textContent = `正在识别:${transcript}`;
commandText.style.color = "#00000085";
}
console.log(transcript, "----");
try {
// 使setTimeoutUI
setTimeout(async () => {
if (transcript) {
const sequence = await this.queryDeepSeek(transcript);
this.showStatus("指令执行完成", "success");
if (sequence && sequence.sequence && sequence.sequence.length > 0) {
this.callback(sequence.sequence);
} else {
this.callback([]);
}
}
this.isProcessing = false;
}, 0);
} catch (error) {
console.error("处理过程中出错:", error);
this.showError(`执行出错: ${error.message}`);
this.isProcessing = false;
}
};
this.recognition.onerror = (event) => {
console.log(event);
this.isListening = false;
this.updateUI();
this.showError(`语音识别错误: ${event.error}`);
document.querySelector(".status-text").textContent = "点击开始语音识别";
this.isProcessing = false;
};
this.recognition.onend = () => {
//
if (this.isListening) {
//
setTimeout(() => {
this.recognition.start();
}, 100); //
} else {
this.updateUI();
document.querySelector(".status-text").textContent = "点击开始语音识别";
setTimeout(() => {
document.querySelector(".status-text").classList.remove("show");
}, 3000);
}
this.isProcessing = false;
// this.isListening = false;
// this.updateUI();
// document.querySelector(".status-text").textContent = "";
// // 3
// setTimeout(() => {
// document.querySelector(".status-text").classList.remove("show");
// }, 3000);
};
}
async queryDeepSeek(userQuery) {
if (!this.apiKey) {
throw new Error("请先设置DeepSeek API密钥");
}
const prompt = `
你是一个网页控制助手请根据以下配置文件分析用户自然语言请求从可用指令集中筛选出与请求意图匹配的指令将用户的自然语言指令解析成一个可执行的指令序列
配置文件
${JSON.stringify(this.config, null, 2)}
用户指令"${userQuery}"
请严格按照以下JSON格式输出只包含一个名为"sequence"的数组
{ "sequence": [ { "command": "command_name", "params": { "param_name": "value" } }, ... ] }
要求
1. 只使用配置文件中定义的command
2.按照符合逻辑的执行顺序对筛选出的指令进行排序例如登录需遵循 "打开登录→输入用户名→输入密码→提交登录" 的顺序
3.仅保留指令的 command 字段形成有序数组
4. 如果用户指令中包含参数值如用户名密码关键词请正确提取并填充到params中
5.若请求涉及多个独立操作需按操作逻辑拆分排序 "先登录再搜索商品" 需包含两部分完整指令链
6.严格禁止添加指令集中不存在的指令无关指令需排除
7.若无可匹配指令返回空数组
现在请生成针对"${userQuery}"的JSON指令序列
`;
const response = await fetch(
"https://api.deepseek.com/v1/chat/completions",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model: "deepseek-chat",
messages: [
{
role: "system",
content: `"你是一个专业的网页语音控制助手。请严格根据提供的配置文件和用户指令,生成准确的操作序列。
重要规则
1. 只返回纯JSON格式不要包含任何其他文本
2. JSON结构必须包含sequence数组\n3. 每个指令必须存在于配置文件中
4. 参数必须匹配指令定义
5. 按逻辑顺序排列指令
6. 若无可匹配指令必须返回空数组
7. 特别注意诗句诗词问候语闲聊内容等与网页操作无关的指令都应返回空数组"`,
},
{
role: "user",
content: prompt,
},
],
temperature: 0.1,
stream: false,
max_tokens: 200,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
`DeepSeek API错误: ${response.status} ${errorData.message || ""}`
);
}
const data = await response.json();
const content = data.choices[0].message.content;
// JSON
let result = JSON.parse(content);
console.log(result, "---result");
//
if (!result.sequence || !Array.isArray(result.sequence)) {
throw new Error("DeepSeek返回了无效的指令序列格式");
}
return result;
}
startListening() {
if (!this.recognition) {
throw new Error("语音识别未初始化");
}
document.querySelector(".status-text").classList.add("show");
this.recognition.start();
}
stopListening() {
if (this.recognition && this.isListening) {
this.recognition.stop();
}
}
// UI
updateUI() {
const voiceBtn = document.getElementById("voice-btn");
if (voiceBtn) {
if (this.isListening) {
voiceBtn.classList.add("listening");
} else {
voiceBtn.classList.remove("listening");
}
}
}
showStatus(message, type = "info") {
const statusEl = document.getElementById("status");
if (statusEl) {
statusEl.textContent = `状态: ${message}`;
statusEl.className = `status ${type}`;
}
}
showTranscript(text) {
const transcriptEl = document.getElementById("transcript");
if (transcriptEl) {
transcriptEl.textContent = `识别结果: ${text}`;
}
}
showError(message) {
this.showStatus(message, "error");
}
}
const voiceControl = ref(null);
const action = ref([]);
const initVoiceControl = () => {
voiceControl.value = new VoiceControl(this.callBackFun);
};
const callBackFun = (data) => {
action.value = data;
console.log(data);
};
const toggleListening = () => {
if (!voiceControl.value) {
alert("请先初始化语音控制系统");
return;
}
if (voiceControl.value.isListening) {
voiceControl.value.stopListening();
} else {
voiceControl.value.startListening();
}
};
onMounted(() => {
initVoiceControl();
});
</script>
<style scoped>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
color: #fff;
}
.container {
max-width: 1200px;
width: 100%;
text-align: center;
padding: 20px;
}
h1 {
font-size: 2.8rem;
margin-bottom: 20px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
p {
font-size: 1.2rem;
max-width: 800px;
margin: 0 auto 30px;
line-height: 1.6;
}
/* 固定在左下角的语音控制组件 */
.voice-control-container {
position: fixed;
width: 144px;
right: 30px;
bottom: 30px;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.voice-btn {
width: 70px;
height: 70px;
border-radius: 50%;
background: linear-gradient(135deg, #6e45e2, #88d3ce);
border: none;
color: white;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
overflow: hidden;
font-size: 0;
}
.voice-btn:hover {
transform: scale(1.05);
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.35);
}
.voice-btn:active {
transform: scale(0.95);
}
.voice-btn.listening {
background: linear-gradient(135deg, #ff5e62, #ff9966);
animation: pulse 1.5s infinite;
font-size: 0;
}
.voice-btn::before {
content: "\f130";
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 28px;
transition: transform 0.3s;
}
.voice-btn.listening::before {
content: "\f131";
animation: bounce 0.5s infinite alternate;
}
.status-text {
background-color: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
color: #00000085;
opacity: 0;
transition: opacity 0.3s;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.status-text.show {
opacity: 1;
}
.pulse-ring {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
opacity: 0;
transform: scale(1);
}
.voice-btn.listening .pulse-ring {
animation: sonar 1.5s infinite;
}
.command-display {
position: fixed;
right: 160px;
bottom: 30px;
background-color: rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10px);
padding: 12px 20px;
border-radius: 12px;
color: #00000080;
max-width: 300px;
opacity: 0;
transform: translateX(-20px);
transition: opacity 0.3s, transform 0.3s;
text-align: left;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.command-display.show {
opacity: 1;
transform: translateX(0);
}
.command-text {
font-size: 16px;
margin-bottom: 5px;
}
.command-action {
font-size: 14px;
color: #88d3ce;
font-weight: 500;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 5px;
background: rgba(255, 255, 255, 0.15);
color: #fff;
}
.info {
background: rgba(0, 123, 255, 0.2);
border-left: 4px solid #007bff;
}
.success {
background: rgba(40, 167, 69, 0.2);
border-left: 4px solid #28a745;
}
.error {
background: rgba(220, 53, 69, 0.2);
border-left: 4px solid #dc3545;
}
.warning {
background: rgba(255, 193, 7, 0.2);
border-left: 4px solid #ffc107;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(255, 94, 98, 0.7);
}
70% {
box-shadow: 0 0 0 15px rgba(255, 94, 98, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(255, 94, 98, 0);
}
}
@keyframes sonar {
0% {
transform: scale(1);
opacity: 0.8;
}
100% {
transform: scale(2.5);
opacity: 0;
}
}
@keyframes bounce {
from {
transform: scale(1);
}
to {
transform: scale(1.3);
}
}
@media (max-width: 768px) {
.voice-control-container {
left: 20px;
bottom: 20px;
}
.command-display {
left: 100px;
max-width: calc(100vw - 120px);
}
h1 {
font-size: 2.2rem;
}
p {
font-size: 1rem;
}
}
</style>

644
src/view/index copy 2.vue Normal file
View File

@ -0,0 +1,644 @@
<!--
* @Author: 季万俊
* @Date: 2025-08-22 17:03:18
* @Description:
-->
<template>
<div class="container">
<!-- 固定在左下角的语音控制组件 -->
<div class="voice-control-container">
<div class="status-text">点击开始语音识别</div>
<button class="voice-btn" id="voice-btn" @click="toggleListening">
<svg
style="
position: absolute;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
"
t="1756171270753"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="14442"
width="42"
height="70"
>
<path
d="M704 192v368c0 52.8-21.6 100.8-56.4 135.6S564.8 752 512 752c-105.6 0-192-86.4-192-192V192C320 86.4 406.4 0 512 0s192 86.4 192 192z"
p-id="14443"
data-spm-anchor-id="a313x.search_index.0.i2.72cc3a81bxN4ca"
class="selected"
fill="#ffffff"
></path>
<path
d="M816 496v144c0 2.8-0.4 5.6-1.1 8.4-18.5 68.2-58.9 126.1-112.3 166.9-43.5 33.2-95.6 55.2-151.6 62.2-4 0.5-7 3.9-7 7.9V944c0 8.8 7.2 16 16 16h80c35.3 0 64 28.7 64 64H320c0-35.3 28.7-64 64-64h80c8.8 0 16-7.2 16-16v-58.5c0-4-3-7.4-7-7.9-124.8-15.7-230.3-105.5-263.9-229.2-0.7-2.7-1.1-5.6-1.1-8.4V496.7c0-17.4 13.7-32.2 31.1-32.7 18.1-0.5 32.9 14 32.9 32v129.8c0 6.9 1.1 13.8 3.3 20.3C309.3 746.9 404.6 816 512 816s202.7-69.1 236.7-169.9c2.2-6.5 3.3-13.4 3.3-20.3V496.7c0-17.4 13.7-32.2 31.1-32.7 18.1-0.5 32.9 14 32.9 32z"
p-id="14444"
data-spm-anchor-id="a313x.search_index.0.i3.72cc3a81bxN4ca"
class="selected"
fill="#ffffff"
></path>
</svg>
<div class="pulse-ring"></div>
</button>
<div class="command-display">
<div class="command-text">我说打开登录</div>
<div class="command-action" v-for="item in action">
{{
"执行操作:" +
`${item.command} ${
item.params && item.params.keyword
? "-" + item.params.keyword
: ""
}`
}}
</div>
</div>
</div>
</div>
</template>
<script>
const config = {
page: "Ecommerce Home",
commands: [
{
command: "open_login",
description: "打开登录弹窗或页面",
action: "click",
selector: "#login-button, .login-btn, [data-login]",
params: [],
},
{
command: "input_username",
description: "在用户名输入框中填写用户名",
action: "input",
selector: "#username, #username-input, input[type='text'][name*='user']",
params: [
{
name: "username",
type: "string",
description: "要输入的用户名",
},
],
},
{
command: "input_password",
description: "在密码输入框中填写密码",
action: "input",
selector: "#password, #password-input, input[type='password']",
params: [
{
name: "password",
type: "string",
description: "要输入的密码",
},
],
},
{
command: "submit_login",
description: "提交登录表单",
action: "click",
selector: "#submit-login, .login-submit, button[type='submit']",
params: [],
},
{
command: "search_product",
description: "在搜索框中输入关键词",
action: "input",
selector: "#search-input, .search-box, input[type='search']",
params: [
{
name: "keyword",
type: "string",
description: "搜索关键词",
},
],
},
{
command: "perform_search",
description: "执行搜索操作",
action: "click",
selector: "#search-button, .search-btn, button[type='submit']",
params: [],
},
{
command: "add_to_cart",
description: "将当前产品添加到购物车",
action: "click",
selector: ".add-to-cart, .cart-btn, [data-add-to-cart]",
params: [],
},
],
};
class VoiceControl {
constructor(callback) {
this.config = config;
this.isListening = false;
this.apiKey = "sk-020189889aac40f3b050f7c60ca597f8";
this.setupSpeechRecognition();
this.updateUI();
this.callback = callback;
}
setupSpeechRecognition() {
const SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
this.showError("您的浏览器不支持语音识别API");
return;
}
this.recognition = new SpeechRecognition();
this.recognition.continuous = true;
this.recognition.interimResults = false;
this.recognition.lang = "zh-CN";
//
this.recognition.energy_threshold = 300; //
this.recognition.pause_threshold = 1.5; //
this.recognition.phrase_threshold = 0.3; //
this.recognition.onstart = () => {
this.isListening = true;
this.updateUI();
this.showStatus("正在聆听...", "info");
document.querySelector(".status-text").textContent = "正在聆听...";
};
this.recognition.onresult = async (event) => {
const transcript = event.results[0][0].transcript;
this.showTranscript(transcript);
this.showStatus("正在处理指令...", "info");
//
const commandDisplay = document.querySelector(".command-display");
commandDisplay.querySelector(
".command-text"
).textContent = `我说:${transcript}`;
commandDisplay.classList.add("show");
console.log(transcript, "----");
if (transcript) {
try {
const sequence = await this.queryDeepSeek(transcript);
this.showStatus("指令执行完成", "success");
console.log(sequence, this.callback, "---------sequence");
//
if (sequence && sequence.sequence && sequence.sequence.length > 0) {
this.callback(sequence.sequence);
} else {
this.callback([]);
}
} catch (error) {
console.error("处理过程中出错:", error);
this.showError(`执行出错: ${error.message}`);
}
}
};
this.recognition.onerror = (event) => {
console.log(event);
this.isListening = false;
this.updateUI();
this.showError(`语音识别错误: ${event.error}`);
document.querySelector(".status-text").textContent = "点击开始语音识别";
};
this.recognition.onend = () => {
this.isListening = false;
this.updateUI();
document.querySelector(".status-text").textContent = "点击开始语音识别";
// 3
setTimeout(() => {
document.querySelector(".status-text").classList.remove("show");
}, 3000);
};
}
async queryDeepSeek(userQuery) {
if (!this.apiKey) {
throw new Error("请先设置DeepSeek API密钥");
}
const prompt = `
你是一个网页控制助手请根据以下配置文件分析用户自然语言请求从可用指令集中筛选出与请求意图匹配的指令将用户的自然语言指令解析成一个可执行的指令序列
配置文件
${JSON.stringify(this.config, null, 2)}
用户指令"${userQuery}"
请严格按照以下JSON格式输出只包含一个名为"sequence"的数组
{ "sequence": [ { "command": "command_name", "params": { "param_name": "value" } }, ... ] }
要求
1. 只使用配置文件中定义的command
2.按照符合逻辑的执行顺序对筛选出的指令进行排序例如登录需遵循 "打开登录→输入用户名→输入密码→提交登录" 的顺序
3.仅保留指令的 command 字段形成有序数组
4. 如果用户指令中包含参数值如用户名密码关键词请正确提取并填充到params中
5.若请求涉及多个独立操作需按操作逻辑拆分排序 "先登录再搜索商品" 需包含两部分完整指令链
6.严格禁止添加指令集中不存在的指令无关指令需排除
7.若无可匹配指令返回空数组
现在请生成针对"${userQuery}"的JSON指令序列
`;
const response = await fetch(
"https://api.deepseek.com/v1/chat/completions",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model: "deepseek-chat",
messages: [
{
role: "system",
content: `"你是一个专业的网页语音控制助手。请严格根据提供的配置文件和用户指令,生成准确的操作序列。
重要规则
1. 只返回纯JSON格式不要包含任何其他文本
2. JSON结构必须包含sequence数组\n3. 每个指令必须存在于配置文件中
4. 参数必须匹配指令定义
5. 按逻辑顺序排列指令
6. 若无可匹配指令必须返回空数组
7. 特别注意诗句诗词问候语闲聊内容等与网页操作无关的指令都应返回空数组"`,
},
{
role: "user",
content: prompt,
},
],
temperature: 0.1,
stream: false,
max_tokens: 1000,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
`DeepSeek API错误: ${response.status} ${errorData.message || ""}`
);
}
const data = await response.json();
const content = data.choices[0].message.content;
// JSON
let result = JSON.parse(content);
console.log(result, "---result");
//
if (!result.sequence || !Array.isArray(result.sequence)) {
throw new Error("DeepSeek返回了无效的指令序列格式");
}
return result;
}
startListening() {
if (!this.recognition) {
throw new Error("语音识别未初始化");
}
document.querySelector(".status-text").classList.add("show");
this.recognition.start();
}
stopListening() {
if (this.recognition && this.isListening) {
this.recognition.stop();
}
}
// UI
updateUI() {
const voiceBtn = document.getElementById("voice-btn");
if (voiceBtn) {
if (this.isListening) {
voiceBtn.classList.add("listening");
} else {
voiceBtn.classList.remove("listening");
}
}
}
showStatus(message, type = "info") {
const statusEl = document.getElementById("status");
if (statusEl) {
statusEl.textContent = `状态: ${message}`;
statusEl.className = `status ${type}`;
}
}
showTranscript(text) {
const transcriptEl = document.getElementById("transcript");
if (transcriptEl) {
transcriptEl.textContent = `识别结果: ${text}`;
}
}
showError(message) {
this.showStatus(message, "error");
}
}
export default {
name: "speechControl",
data() {
return {
voiceControl: null,
action: [],
};
},
props: {
config: {
type: Object,
default: () => {},
},
},
watch: {
config: {
handler() {
if (this.voiceControl && this.voiceControl.isListening) {
this.voiceControl.stopListening();
}
this.initVoiceControl();
},
immediate: true,
},
},
mounted() {
this.initVoiceControl();
},
methods: {
initVoiceControl() {
this.voiceControl = new VoiceControl(this.callBackFun);
},
callBackFun(data) {
this.action = data;
console.log(data);
},
toggleListening() {
if (!this.voiceControl) {
alert("请先初始化语音控制系统");
return;
}
if (this.voiceControl.isListening) {
this.voiceControl.stopListening();
} else {
this.voiceControl.startListening();
}
},
},
};
</script>
<style scoped>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
color: #fff;
}
.container {
max-width: 1200px;
width: 100%;
text-align: center;
padding: 20px;
}
h1 {
font-size: 2.8rem;
margin-bottom: 20px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
p {
font-size: 1.2rem;
max-width: 800px;
margin: 0 auto 30px;
line-height: 1.6;
}
/* 固定在左下角的语音控制组件 */
.voice-control-container {
position: fixed;
width: 144px;
right: 30px;
bottom: 30px;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.voice-btn {
width: 70px;
height: 70px;
border-radius: 50%;
background: linear-gradient(135deg, #6e45e2, #88d3ce);
border: none;
color: white;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
overflow: hidden;
font-size: 0;
}
.voice-btn:hover {
transform: scale(1.05);
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.35);
}
.voice-btn:active {
transform: scale(0.95);
}
.voice-btn.listening {
background: linear-gradient(135deg, #ff5e62, #ff9966);
animation: pulse 1.5s infinite;
font-size: 0;
}
.voice-btn::before {
content: "\f130";
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 28px;
transition: transform 0.3s;
}
.voice-btn.listening::before {
content: "\f131";
animation: bounce 0.5s infinite alternate;
}
.status-text {
background-color: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
color: #00000085;
opacity: 0;
transition: opacity 0.3s;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.status-text.show {
opacity: 1;
}
.pulse-ring {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
opacity: 0;
transform: scale(1);
}
.voice-btn.listening .pulse-ring {
animation: sonar 1.5s infinite;
}
.command-display {
position: fixed;
right: 160px;
bottom: 30px;
background-color: rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10px);
padding: 12px 20px;
border-radius: 12px;
color: #00000080;
max-width: 300px;
opacity: 0;
transform: translateX(-20px);
transition: opacity 0.3s, transform 0.3s;
text-align: left;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.command-display.show {
opacity: 1;
transform: translateX(0);
}
.command-text {
font-size: 16px;
margin-bottom: 5px;
}
.command-action {
font-size: 14px;
color: #88d3ce;
font-weight: 500;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 5px;
background: rgba(255, 255, 255, 0.15);
color: #fff;
}
.info {
background: rgba(0, 123, 255, 0.2);
border-left: 4px solid #007bff;
}
.success {
background: rgba(40, 167, 69, 0.2);
border-left: 4px solid #28a745;
}
.error {
background: rgba(220, 53, 69, 0.2);
border-left: 4px solid #dc3545;
}
.warning {
background: rgba(255, 193, 7, 0.2);
border-left: 4px solid #ffc107;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(255, 94, 98, 0.7);
}
70% {
box-shadow: 0 0 0 15px rgba(255, 94, 98, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(255, 94, 98, 0);
}
}
@keyframes sonar {
0% {
transform: scale(1);
opacity: 0.8;
}
100% {
transform: scale(2.5);
opacity: 0;
}
}
@keyframes bounce {
from {
transform: scale(1);
}
to {
transform: scale(1.3);
}
}
@media (max-width: 768px) {
.voice-control-container {
left: 20px;
bottom: 20px;
}
.command-display {
left: 100px;
max-width: calc(100vw - 120px);
}
h1 {
font-size: 2.2rem;
}
p {
font-size: 1rem;
}
}
</style>

316
src/view/index copy.vue Normal file
View File

@ -0,0 +1,316 @@
<template>
<div class="gpt-base-container">
<!-- Heading -->
<h1 class="gap">whisper-base</h1>
<!-- 文件选择表单 -->
<form
id="myForm"
class="gap form-container"
@submit.prevent="handleSubmit"
>
<label for="inputAudioFile">选择音频文件</label>
<input
type="file"
id="inputAudioFile"
ref="fileInput"
@change="handleFileChange"
accept="audio/*"
>
<button
type="submit"
id="submitButton"
:disabled="!audioFile || isProcessing"
class="submit-btn"
>
{{ isProcessing ? '处理中...' : '生成' }}
</button>
</form>
<!-- 音频播放器 -->
<audio
id="audioBar"
controls
class="gap"
:src="audioUrl"
ref="audioPlayer"
></audio>
<!-- 输出结果 -->
<div
id="outputDiv"
class="output-container gap"
>
{{ outputText || 'Waiting...' }}
<span v-if="isTyping && outputText" class="typing-cursor">_</span>
</div>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue';
export default {
name: 'WhisperBase',
setup() {
//
const audioFile = ref(null);
const audioUrl = ref(null);
const outputText = ref('Waiting...');
const isProcessing = ref(false);
const isTyping = ref(false);
// DOM
const fileInput = ref(null);
const audioPlayer = ref(null);
//
let typingTimer = null;
//
onMounted(() => {
//
console.log('WhisperBase component mounted');
});
onUnmounted(() => {
//
if (typingTimer) {
clearTimeout(typingTimer);
}
// URL
if (audioUrl.value) {
URL.revokeObjectURL(audioUrl.value);
}
});
//
const handleFileChange = (event) => {
const file = event.target.files[0];
if (file) {
// URL
if (audioUrl.value) {
URL.revokeObjectURL(audioUrl.value);
}
audioFile.value = file;
audioUrl.value = URL.createObjectURL(file);
outputText.value = '文件已选择,点击生成按钮开始处理';
}
};
//
const handleSubmit = async (event) => {
event.preventDefault();
if (!audioFile.value) {
alert("请选择文件!");
return;
}
if (isProcessing.value) {
return;
}
isProcessing.value = true;
outputText.value = '准备生成...';
try {
// transformers
const { pipeline, env } = await import('/src/model-base/transformers@2.4.1');
//
env.allowRemoteModels = false;
env.localModelPath = '/src/model-base/';
env.backends.onnx.wasm.wasmPaths = '/src/model-base/';
//
const transcriber = await pipeline('automatic-speech-recognition', 'Xenova/whisper-base');
//
const output = await transcriber(audioUrl.value, {
chunk_length_s: 30,
return_timestamps: true
});
//
startTypingEffect(output.text);
} catch (error) {
console.error('处理失败:', error);
outputText.value = `处理失败: ${error.message}`;
} finally {
isProcessing.value = false;
}
};
//
const startTypingEffect = (text) => {
let i = 0;
isTyping.value = true;
//
if (typingTimer) {
clearTimeout(typingTimer);
}
const type = () => {
if (i <= text.length) {
outputText.value = text.slice(0, i++);
typingTimer = setTimeout(type, 50);
} else {
isTyping.value = false;
outputText.value = text; //
}
};
type();
};
//
const resetForm = () => {
if (fileInput.value) {
fileInput.value.value = '';
}
audioFile.value = null;
if (audioUrl.value) {
URL.revokeObjectURL(audioUrl.value);
audioUrl.value = null;
}
outputText.value = 'Waiting...';
if (audioPlayer.value) {
audioPlayer.value.src = '';
}
};
//
return {
audioFile,
audioUrl,
outputText,
isProcessing,
isTyping,
fileInput,
audioPlayer,
handleFileChange,
handleSubmit,
resetForm
};
}
};
</script>
<style scoped>
.gpt-base-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.gap {
padding: 10px 40px;
}
.form-container {
border: cadetblue solid 5px;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
.form-container label {
font-weight: bold;
margin-bottom: 5px;
}
.form-container input[type="file"] {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.submit-btn {
width: 100px;
font-size: 1.5em;
color: cadetblue;
background: white;
border: 2px solid cadetblue;
border-radius: 5px;
padding: 10px 20px;
cursor: pointer;
transition: all 0.3s ease;
}
.submit-btn:hover:not(:disabled) {
background: cadetblue;
color: white;
}
.submit-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.output-container {
background-color: bisque;
border-radius: 5px;
min-height: 100px;
padding: 20px;
font-size: 16px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.typing-cursor {
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
/* 响应式设计 */
@media (max-width: 768px) {
.gap {
padding: 10px 20px;
}
.form-container {
padding: 15px;
}
.submit-btn {
width: 80px;
font-size: 1.2em;
padding: 8px 16px;
}
}
/* 音频播放器样式 */
audio {
width: 100%;
margin: 20px 0;
}
/* 文件输入样式 */
input[type="file"] {
padding: 10px;
border: 2px dashed #ccc;
border-radius: 8px;
background: #f9f9f9;
transition: border-color 0.3s ease;
}
input[type="file"]:hover {
border-color: cadetblue;
}
</style>

431
src/view/index.vue Normal file
View File

@ -0,0 +1,431 @@
<template>
<div class="app-container">
<!-- 左侧导航栏 -->
<aside class="sidebar">
<!-- 用户信息 -->
<div class="user-section">
<div class="avatar"></div>
<div class="user-id">1735244688</div>
<div class="user-label">用户ID</div>
</div>
<div class="line"></div>
<!-- 导航菜单 -->
<nav class="nav-menu">
<ul>
<li
@click="selectMenu(item)"
v-for="item in menu"
:class="item.active ? 'active' : ''"
>
<el-icon :size="20"><component :is="item.icon" /></el-icon>
<span>{{ item.name }}</span>
</li>
</ul>
</nav>
<!-- 广告卡片 -->
<div class="ad-card">
<div class="ad-img"></div>
<div class="ad-title">大幅面优化 倾斜摄影卡顿</div>
<button class="ad-btn">前往下载</button>
</div>
<!-- 底部链接 -->
<div class="footer-links">
<a href="#">SDK文档</a>
<a href="#">视频教程</a>
<a href="#">人工客服</a>
<a href="#">建议反馈</a>
</div>
</aside>
<!-- 主内容区 -->
<main class="main-content">
<!-- 顶部横幅 -->
<section class="header-banner">
<div class="banner-content">
<h1>可视化</h1>
<p>零代码数字孪生可视化平台</p>
<div class="tag-group">
<span>可视化大屏</span>
<span>三维地图</span>
<span>GIS</span>
<span>数字孪生</span>
</div>
</div>
</section>
<!-- 项目操作栏 -->
<section class="project-actions">
<div class="action-buttons">
<button>新建</button>
<button>新建文件夹</button>
<button>导入项目</button>
</div>
<div class="tab-group">
<span class="active">本地项目</span>
<span>云托管项目</span>
</div>
<div class="search-box">
<input type="text" placeholder="搜索项目" />
</div>
</section>
<!-- 项目列表 -->
<section class="project-list">
<div class="project-card">
<div class="card-img"></div>
<div class="card-title">我的驾驶大屏示例</div>
<div class="card-actions">
<span>编辑</span>
<span>复制</span>
<span>删除</span>
</div>
</div>
<div class="project-card">
<div class="card-img"></div>
<div class="card-title">我的智慧城市3D场景</div>
<div class="card-actions">
<span>编辑</span>
<span>复制</span>
<span>删除</span>
</div>
</div>
</section>
</main>
</div>
</template>
<script setup >
import { ref } from "vue";
const activeValue = ref(1);
const menu = ref([
{
name: "我的项目",
icon: "DataAnalysis",
active: true,
},
{
name: "报表门户",
icon: "TrendCharts",
active: false,
},
{
name: "回收站",
icon: "Delete",
active: false,
},
{
name: "资源中心",
icon: "Stopwatch",
active: false,
},
{
name: "私有云",
icon: "MostlyCloudy",
active: false,
},
{
name: "设置中心",
icon: "Setting",
active: false,
},
]);
const selectMenu = (data) => {
menu.value.forEach((d) => {
if (d.name === data.name) {
d.active = true;
} else {
d.active = false;
}
});
};
</script>
<style lang="less" scoped>
/* 全局布局 */
.app-container {
display: flex;
height: 100vh;
background-color: #1e1e1e;
color: #ffffff;
font-family: "Microsoft YaHei", sans-serif;
}
/* 左侧导航栏 */
.sidebar {
width: 200px;
padding: 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #181818;
}
/* 用户信息区 */
.user-section {
text-align: center;
margin-bottom: 20px;
}
.avatar {
width: 60px;
height: 60px;
background: #444;
border-radius: 50%;
margin: 0 auto 8px;
}
.user-id {
font-size: 14px;
font-weight: 500;
}
.user-label {
font-size: 12px;
color: #999;
}
/* 导航菜单 */
.nav-menu ul {
list-style: none;
padding: 0;
height: 400px;
}
.nav-menu li {
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
padding: 8px 12px;
margin: 4px 0;
box-sizing: border-box;
display: flex;
color: #ffffff99;
.el-icon {
margin-right: 6px;
line-height: 20px;
}
}
.nav-menu li:hover {
background: rgba(5, 85, 158, 0.4);
}
.nav-menu li.active {
background: rgb(5, 85, 158);
font-weight: 500;
}
.divider {
height: 1px;
background: #333;
padding: 0 !important;
margin: 6px 0;
}
/* 广告卡片 */
.ad-card {
background: #282828;
background: rgb(26, 27, 29);
padding: 16px;
border-radius: 8px;
text-align: center;
}
.ad-img {
width: 80px;
height: 80px;
background: #444;
border-radius: 4px;
margin: 0 auto 12px;
}
.ad-title {
font-size: 14px;
margin-bottom: 12px;
}
.ad-btn {
background: #0078d4;
border: none;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
color: #fff;
transition: background 0.3s;
}
.ad-btn:hover {
background: #005a9e;
}
/* 底部链接 */
.footer-links {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #999;
}
.footer-links a {
color: #999;
text-decoration: none;
transition: color 0.3s;
}
.footer-links a:hover {
color: #fff;
}
/* 主内容区 */
.main-content {
flex: 1;
padding: 0;
}
/* 顶部横幅 */
.header-banner {
width: 100%;
height: 300px;
position: relative;
text-align: center;
margin-bottom: 24px;
background-image: url("@/assets/img/banner.png");
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
&::before {
content: "";
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
position: absolute;
z-index: 9;
}
h1 {
margin: 0;
}
.banner-content {
position: absolute;
z-index: 20;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
}
}
.header-banner h1 {
font-size: 36px;
margin-bottom: 8px;
}
.header-banner p {
font-size: 16px;
color: #999;
margin-bottom: 16px;
position: relative;
&::before {
content: "";
width: 200px;
height: 1px;
background: rgba(255, 255, 255, 0.9);
position: absolute;
left: -220px;
top: 50%;
transform: translateY(-50%);
}
&::after {
content: "";
width: 200px;
height: 1px;
background: rgba(255, 255, 255, 0.9);
position: absolute;
right: -220px;
top: 50%;
transform: translateY(-50%);
}
}
.tag-group span {
margin: 0 6px;
color: #999;
font-size: 14px;
}
/* 项目操作栏 */
.project-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.action-buttons button {
background: rgb(26, 27, 29);
border: 1px solid #444;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
color: #fff;
margin-right: 8px;
transition: background 0.3s;
}
.action-buttons button:hover {
background: #383838;
}
.tab-group span {
margin: 0 12px;
cursor: pointer;
transition: color 0.3s;
}
.tab-group span:hover {
color: #0078d4;
}
.tab-group span.active {
color: #0078d4;
font-weight: 500;
}
.search-box input {
background: rgb(26, 27, 29);
border: 1px solid #444;
padding: 8px 16px;
border-radius: 4px;
color: #fff;
outline: none;
}
.search-box input::placeholder {
color: #999;
}
/* 项目列表 */
.project-list {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.project-card {
width: 240px;
background: rgb(26, 27, 29);
border-radius: 8px;
padding: 16px;
text-align: center;
transition: transform 0.3s;
}
.project-card:hover {
transform: translateY(-5px);
}
.card-img {
width: 100%;
height: 120px;
background: #444;
border-radius: 4px;
margin-bottom: 12px;
}
.card-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
}
.card-actions {
color: #999;
}
.card-actions span {
margin: 0 6px;
cursor: pointer;
transition: color 0.3s;
}
.card-actions span:hover {
color: #0078d4;
}
.line {
width: 160px;
background: rgb(15, 15, 15);
}
</style>

28
vite.config.js Normal file
View File

@ -0,0 +1,28 @@
/*
* @Author: 季万俊
* @Date: 2025-06-10 14:36:16
* @Description:
*/
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
base: '',
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
host: '0.0.0.0', // 关键允许通过IP访问监听所有网络接口
port: 8080, // 可选指定端口默认5173
open: true, // 可选:启动时自动打开浏览器,
proxy: {
}
}
})