feat(i18n) multi-lang on uart page: Chinese, French and English.

This commit is contained in:
kerms 2024-09-23 16:01:46 +02:00
parent dd1b9adc0d
commit d7d7c94f53
14 changed files with 637 additions and 204 deletions

95
package-lock.json generated
View File

@ -10,7 +10,7 @@
"dependencies": { "dependencies": {
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",
"ansi_up": "^6.0.2", "ansi_up": "^6.0.2",
"element-plus": "^2.7.3", "element-plus": "^2.8.1",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",
@ -979,31 +979,29 @@
} }
}, },
"node_modules/@volar/language-core": { "node_modules/@volar/language-core": {
"version": "2.1.4", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.0.tgz",
"integrity": "sha512-ROfPepDxZ5Eq+Unbx3M9QcHT7MoE9tYdbkuzLTtxG5rfkEi5RwsDPncjANMOq/gHhIIDlWgqWwS2nXWMGsuj4w==", "integrity": "sha512-FTla+khE+sYK0qJP+6hwPAAUwiNHVMph4RUXpxf/FIPKUP61NFrVZorml4mjFShnueR2y9/j8/vnh09YwVdH7A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@volar/source-map": "2.1.4" "@volar/source-map": "2.4.0"
} }
}, },
"node_modules/@volar/source-map": { "node_modules/@volar/source-map": {
"version": "2.1.4", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.0.tgz",
"integrity": "sha512-mCg8IiPZmHZVzqL4Owg+BzQ5ZTG1cVwATxrkrFPZpcAin97Xa3MbchxVhHtHTWTT8ER8bJh5xVjeVxsSN++FUA==", "integrity": "sha512-2ceY8/NEZvN6F44TXw2qRP6AQsvCYhV2bxaBPWxV9HqIfkbRydSksTFObCF1DBDNBfKiZTS8G/4vqV6cvjdOIQ==",
"dev": true, "dev": true
"dependencies": {
"muggle-string": "^0.4.0"
}
}, },
"node_modules/@volar/typescript": { "node_modules/@volar/typescript": {
"version": "2.1.4", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.0.tgz",
"integrity": "sha512-Mt7wOLPkomFnUfVpb5IHlPhSpD7FJAn+FHSsovePmqFNQzFLz16wrpHjAkorPiAnP0847w71NL5fIJyWbAsR8Q==", "integrity": "sha512-9zx3lQWgHmVd+JRRAHUSRiEhe4TlzL7U7e6ulWXOxHH/WNYxzKwCvZD7WYWEZFdw4dHfTD9vUR0yPQO6GilCaQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@volar/language-core": "2.1.4", "@volar/language-core": "2.4.0",
"path-browserify": "^1.0.1" "path-browserify": "^1.0.1",
"vscode-uri": "^3.0.8"
} }
}, },
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
@ -1052,6 +1050,16 @@
"@vue/shared": "3.4.21" "@vue/shared": "3.4.21"
} }
}, },
"node_modules/@vue/compiler-vue2": {
"version": "2.7.16",
"resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
"integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
"dev": true,
"dependencies": {
"de-indent": "^1.0.2",
"he": "^1.2.0"
}
},
"node_modules/@vue/devtools-api": { "node_modules/@vue/devtools-api": {
"version": "6.6.1", "version": "6.6.1",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.1.tgz", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.1.tgz",
@ -1096,18 +1104,19 @@
} }
}, },
"node_modules/@vue/language-core": { "node_modules/@vue/language-core": {
"version": "2.0.7", "version": "2.0.29",
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.29.tgz",
"integrity": "sha512-Vh1yZX3XmYjn9yYLkjU8DN6L0ceBtEcapqiyclHne8guG84IaTzqtvizZB1Yfxm3h6m7EIvjerLO5fvOZO6IIQ==", "integrity": "sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@volar/language-core": "~2.1.3", "@volar/language-core": "~2.4.0-alpha.18",
"@vue/compiler-dom": "^3.4.0", "@vue/compiler-dom": "^3.4.0",
"@vue/compiler-vue2": "^2.7.16",
"@vue/shared": "^3.4.0", "@vue/shared": "^3.4.0",
"computeds": "^0.0.1", "computeds": "^0.0.1",
"minimatch": "^9.0.3", "minimatch": "^9.0.3",
"path-browserify": "^1.0.1", "muggle-string": "^0.4.1",
"vue-template-compiler": "^2.7.14" "path-browserify": "^1.0.1"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "*" "typescript": "*"
@ -1977,9 +1986,9 @@
"dev": true "dev": true
}, },
"node_modules/element-plus": { "node_modules/element-plus": {
"version": "2.7.3", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.7.3.tgz", "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.8.1.tgz",
"integrity": "sha512-OaqY1kQ2xzNyRFyge3fzM7jqMwux+464RBEqd+ybRV9xPiGxtgnj/sVK4iEbnKnzQIa9XK03DOIFzoToUhu1DA==", "integrity": "sha512-p11/6w/O0+hGvPhiN3jrcgh+XG+eg5jZlLdQVYvcPHZYhhCh3J3YeZWW1JO/REPES1vevkboT6VAi+9wHA8Dsg==",
"dependencies": { "dependencies": {
"@ctrl/tinycolor": "^3.4.1", "@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
@ -3249,9 +3258,9 @@
} }
}, },
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.7", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"braces": "^3.0.3", "braces": "^3.0.3",
@ -5042,6 +5051,12 @@
"vue": ">=3.2.13" "vue": ">=3.2.13"
} }
}, },
"node_modules/vscode-uri": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
"dev": true
},
"node_modules/vue": { "node_modules/vue": {
"version": "3.4.21", "version": "3.4.21",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz",
@ -5132,31 +5147,21 @@
"vue": "^3.2.0" "vue": "^3.2.0"
} }
}, },
"node_modules/vue-template-compiler": {
"version": "2.7.16",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
"integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
"dev": true,
"dependencies": {
"de-indent": "^1.0.2",
"he": "^1.2.0"
}
},
"node_modules/vue-tsc": { "node_modules/vue-tsc": {
"version": "2.0.7", "version": "2.0.29",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.7.tgz", "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.29.tgz",
"integrity": "sha512-LYa0nInkfcDBB7y8jQ9FQ4riJTRNTdh98zK/hzt4gEpBZQmf30dPhP+odzCa+cedGz6B/guvJEd0BavZaRptjg==", "integrity": "sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@volar/typescript": "~2.1.3", "@volar/typescript": "~2.4.0-alpha.18",
"@vue/language-core": "2.0.7", "@vue/language-core": "2.0.29",
"semver": "^7.5.4" "semver": "^7.5.4"
}, },
"bin": { "bin": {
"vue-tsc": "bin/vue-tsc.js" "vue-tsc": "bin/vue-tsc.js"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "*" "typescript": ">=5.0.0"
} }
}, },
"node_modules/vuetify": { "node_modules/vuetify": {

View File

@ -18,7 +18,7 @@
"dependencies": { "dependencies": {
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",
"ansi_up": "^6.0.2", "ansi_up": "^6.0.2",
"element-plus": "^2.7.3", "element-plus": "^2.8.1",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"/></svg>

After

Width:  |  Height:  |  Size: 411 B

View File

@ -1,19 +1,48 @@
import { createI18n } from 'vue-i18n'; import {createI18n} from 'vue-i18n';
import zh from '@/locales/zh' import zh from '@/locales/zh'
import en from '@/locales/en' import en from '@/locales/en'
import fr from '@/locales/fr'
// const locale = localStorage.getItem('lang') || 'zh'; const userLanguage = navigator.language || 'en';
export const locale = 'zh';
// Get the language code (e.g., 'en' from 'en-US')
export const locale = userLanguage.split('-')[0];
const messages = {
zh,
en,
fr,
} as const;
type Locale = keyof typeof messages;
export const availableLanguages = Object.keys(messages);
// export const locale = 'zh';
console.log(userLanguage, locale, availableLanguages)
const i18n = createI18n({ const i18n = createI18n({
globalInjection: true, globalInjection: true,
legacy: false, legacy: false,
locale: locale, locale: locale,
fallbackLocale: 'zh', fallbackLocale: 'zh',
messages: { messages: messages
zh,
// en,
}
}); });
export function getFlagFromLang(lang: string) {
if (lang === 'zh') {
return '🇨🇳';
} else if (lang === 'en') {
return '🇺🇸';
} else if (lang === 'fr') {
return '🇫🇷';
}
return '🏳️';
}
export function setLang(lang: string): void {
if (availableLanguages.includes(lang)) {
i18n.global.locale.value = lang as Locale;
}
}
export default i18n; export default i18n;

View File

@ -1,3 +1,127 @@
export default { export default {
disconnected: "disconnected" emoji: {
flag: "🇺🇸",
},
disconnected: "Disconnected",
connected: "Connected",
connecting: "Connecting",
ws: {
disconnected: "Disconnected",
connected: "Connected",
connecting: "Connecting",
},
page: {
home: "Home",
wifi: "Wi-Fi",
about: "About",
uart: "Uart",
feedback: "Feedback",
close: "Close",
update: "Update",
fullscreen: "Fullscreen",
windowed: "Windowed"
},
uart: {
port: "Port",
startCommunication: "Start Communication",
stopCommunication: "Stop Communication",
commonlyUsed: "Common",
baudrate: "Baud Rate",
customBaud: "Custom Baud",
use: "Use",
actual: "Actual",
dataBits: "Data Bits",
stopBits: "Stop Bits",
parity: "Parity",
parityNone: "None",
parityOdd: "Odd",
parityEven: "Even",
flowControl: "Flow Control",
send: "Send",
clear: "Clear",
clearTooltip: "Only clears the display area, can be restored with refresh.",
updateTooltip: "Sync with cache + filter",
autoUpdateTooltip: "Only stop refreshing the display area; the background continues to receive data.",
receive: "Receive",
displayOptions: "Display Options",
display: "Display",
show: "Show",
text: "Text",
timestamp: "Timestamp",
enable: "Enable",
lineWrap: "Line Wrap",
highlight: "Highlight",
frameBreakStrategy: "Frame Break Strategy",
priority: "Priority",
rule: "Rule",
ruleTips:
"<p>Timeout=-1: Disable timeout frame break</p>" +
"<p>Timeout=0: Immediate break, any received data is considered complete</p>" +
"<p>Match after break: Typical \\n scenario</p>" +
"<p>Match before break: For scenarios with special frame headers</p>" +
"<p>Fixed byte frame break: Useful for large data transfer, e.g., break frame every 1024 bytes for easy data viewing</p>",
value: "Value",
timeout: "Timeout",
match: "Match",
byte: "Byte",
begin: "b",
end: "b",
other: "Other",
decodeAnsiEscapeCodes: "Decode ANSI Escape Codes",
ansiTooltips:
"<p>ANSI escape codes have many uses for terminals and text, such as changing text colors, among other effects.</p>\n" +
"<p>\n Learn more ->\n <a target=\"_blank\" href=\"https://en.wikipedia.org/wiki/ANSI_escape_code\">" +
"https://en.wikipedia.org/wiki/ANSI_escape_code\n </a></p>",
filter: "Filter",
textAndEscape: "Text with \\n\\x support",
autoUpdateNewData: "Auto-refresh new data",
updateFrequency: "Data Display Update Interval (ms)",
updateFrequencyTooltip: "Increasing the interval can reduce CPU usage.",
addHeader: "Add Header",
addFooter: "Add Footer",
passthrough: "Passthrough",
proxy: "Proxy",
serverPort: "Server Port",
connectedClient: "Connected Client",
refresh: "Refresh",
interface: "Interface",
noClientConnected: "No Client Connected",
import: "Import",
export: "Export",
reset: "Reset",
resetTooltip: "Takes effect after refreshing the page.",
saveToLocal: "Save to Local",
saveToLocalTooltip: "If multiple pages exist, they will overwrite each other.",
add: "Add",
edit: "Edit",
drag: "Drag",
ipChangeAlert: "Changing the IP address will cause the configuration to be lost.",
layout: "Layout",
landscape: "Landscape",
portrait: "Portrait",
responsive: "Responsive",
configPannel: "Config",
displayPannel: "Display",
macroPannel: "Quick Send",
autoScrollToBottom: "Auto Scroll",
clearScreen: "Clear",
autoUpdate: "Auto Update",
tempDisplayTooltip: "Data that does not meet the frame-break rules (e.g., not timed out) is temporarily displayed in real-time in this area. If it exceeds 8192 bytes, it will automatically break frames.",
loopSend: "Loop Send",
loopSendTooltip: "The actual frequency is affected by the interface refresh rate. For more accuracy, you can try turning off 'auto-refresh'.",
sendFormat: "Send Format",
cachedFrame: "Cached",
format: "Format",
}
}; };

128
src/locales/fr.ts Normal file
View File

@ -0,0 +1,128 @@
export default {
emoji: {
flag: "🇫🇷",
},
disconnected: "Déconnecté",
connected: "Connecté",
connecting: "Connexion..",
ws: {
disconnected: "Déconnecté",
connected: "Connecté",
connecting: "Connexion..",
},
page: {
home: "Accueil",
wifi: "Wi-Fi",
about: "À propos",
uart: "Uart",
feedback: "Feedback",
close: "Fermer",
update: "Mise à jour",
fullscreen: "Plein écran",
windowed: "Fenêtré",
},
uart: {
port: "Port",
startCommunication: "Démarrer la communication",
stopCommunication: "Arrêter la communication",
commonlyUsed: "Fréquemment utilisé",
baudrate: "Taux de Baud",
customBaud: "Baud",
use: "Utiliser",
actual: "Actuel",
dataBits: "Bits de Données",
stopBits: "Bits d'Arrêt",
parity: "Parité",
parityNone: "Aucune",
parityOdd: "Impair(Odd)",
parityEven: "Pair(Even)",
flowControl: "Contrôle de Flux",
send: "Envoyer",
clear: "Effacer",
clearTooltip: "Ne supprime que la zone d'affichage, peut être restaurée en actualisant.",
updateTooltip: "Synchroniser avec le cache + filtrer",
autoUpdateTooltip: "Arrête uniquement le rafraîchissement de la zone d'affichage ; l'arrière-plan continue de recevoir des données.",
receive: "Recevoir",
displayOptions: "Options d'Affichage",
display: "Affichage",
show: "Afficher",
text: "Texte",
timestamp: "Horodatage",
enable: "Activer",
lineWrap: "Retour à la Ligne",
highlight: "Surligner",
frameBreakStrategy: "Stratégie de Coupure de Trame",
priority: "Priorité",
rule: "Règle",
ruleTips:
"<p>Délai d'expiration=-1 : Désactiver la coupure de trame par délai d'expiration</p>" +
"<p>Délai d'expiration=0 : Coupure immédiate, toutes données reçues sont considérées complètes</p>" +
"<p>Match après coupure : Scénario typique \\n</p>" +
"<p>Match avant coupure : Pour des scénarios avec en-têtes de trame spécifiques</p>" +
"<p>Coupure de trame par octets fixes : Utile pour le transfert de grandes quantités de données, par exemple, couper la trame tous les 1024 octets pour faciliter la visualisation des données</p>",
value: "Valeur",
timeout: "Timeout",
match: "Match",
byte: "Byte",
begin: "b",
end: "b",
other: "Autres",
decodeAnsiEscapeCodes: "Décode Échappement ANSI",
ansiTooltips:
"<p>Les codes d'échappement ANSI ont de nombreuses utilisations pour les terminaux et le texte, comme changer les couleurs du texte, entre autres effets.</p>" +
"<p>\n En savoir plus ->\n " +
"<a target=\"_blank\" href=\"https://en.wikipedia.org/wiki/ANSI_escape_code\">\n" +
"https://en.wikipedia.org/wiki/ANSI_escape_code\n </a>\n</p>",
filter: "Filtrer",
textAndEscape: "Texte;supporte\\n\\x",
autoUpdateNewData: "Auto-update nouvelles données",
updateFrequency: "Délais rafraîchissement des Données (ms)",
updateFrequencyTooltip: "Augmenter l'intervalle peut réduire l'utilisation des ressources CPU.",
addHeader: "Ajouter un En-tête",
addFooter: "Ajouter un Pied de page",
passthrough: "Passage Direct",
proxy: "Proxy",
serverPort: "Port Serveur",
connectedClient: "Client Connecté",
refresh: "Rafraîchir",
interface: "Interface",
noClientConnected: "Aucun Client Connecté",
import: "Importer",
export: "Exporter",
reset: "Réinitialiser",
resetTooltip: "Prend effet après le rafraîchissement de la page.",
saveToLocal: "Enregistrer Localement",
saveToLocalTooltip: "S'il existe plusieurs pages, elles se chevaucheront mutuellement.",
add: "Ajouter",
edit: "Éditer",
drag: "Glisser",
ipChangeAlert: "Le changement d'adresse IP entraînera la perte de la configuration.",
layout: "Disposition",
landscape: "Paysage",
portrait: "Portrait",
responsive: "Résponsive",
configPannel: "Configuration",
displayPannel: "Données",
macroPannel: "Envoie Rapide",
autoScrollToBottom: "Auto Scroll",
clearScreen: "Effacer",
autoUpdate: "Auto Update",
tempDisplayTooltip: "es données qui ne respectent pas les règles de rupture de trame (par exemple : non expirées) s'affichent temporairement en temps réel dans cette zone. Au-delà de 8192 octets, une rupture de trame est automatique.",
loopSend: "Envoi en Boucle",
loopSendTooltip: "La fréquence réelle est influencée par le taux de rafraîchissement de l'interface. Pour plus de précision, vous pouvez essayer de désactiver 'l'actualisation automatique'.",
sendFormat: "Format d'Envoi",
cachedFrame: "Cache",
format: "Format",
}
};

View File

@ -7,8 +7,8 @@ type NestedKeyOf<ObjectType extends object> = {
: `${Key}` : `${Key}`
}[keyof ObjectType & (string | number)]; }[keyof ObjectType & (string | number)];
type TranslationKeys = NestedKeyOf<typeof zh>; export type TranslationKeys = NestedKeyOf<typeof zh>;
export function translate<K extends TranslationKeys>(key: K | string): string { export function translate(key: TranslationKeys | string): string {
return i18n.global.t(key.toLowerCase()); return i18n.global.t(key);
} }

View File

@ -1,7 +1,11 @@
export default { export default {
emoji: {
flag: "🇨🇳",
},
disconnected: "未连接", disconnected: "未连接",
connected: "已连接", connected: "已连接",
connecting: "连接中", connecting: "连接中",
use: "使用",
ws: { ws: {
disconnected: "未连接", disconnected: "未连接",
@ -13,9 +17,115 @@ export default {
home: "主页", home: "主页",
wifi: "Wi-Fi", wifi: "Wi-Fi",
about: "关于", about: "关于",
uart: "UART透传", uart: "UART",
feedback: "反馈", feedback: "反馈",
close: "关闭", close: "关闭",
update: "更新", update: "更新",
fullscreen: "全屏",
windowed: "窗口",
}, },
uart: {
port: "接口",
startCommunication: "开始数据收发",
stopCommunication: "停止数据收发",
commonlyUsed: "常用",
baudrate: "波特率",
customBaud: "自定义波特率",
use: "使用",
actual: "实际",
dataBits: "数据位",
stopBits: "停止位",
parity: "校验位",
parityNone: "无(None)",
parityOdd: "奇(Odd)",
parityEven: "偶(Even)",
flowControl: "流控制",
send: "发送",
clear: "清空",
clearTooltip: "仅清除显示区域,可用刷新恢复",
updateTooltip: "与缓存同步+过滤",
autoUpdateTooltip: "仅停止刷新显示区,后台继续接收数据",
receive: "接收",
displayOptions: "显示选项",
display: "显示框",
show: "显示",
text: "文本",
timestamp: "时间戳",
enable: "启用",
lineWrap: "换行",
highlight: "高亮",
frameBreakStrategy: "断帧策略",
priority: "优先级",
rule: "规则",
ruleTips:
"<p>超时=-1 禁用超时断帧</p>" +
"<p>超时=0 当机立断,收到任何数据都视为完整数据</p>" +
"<p>匹配断后:典型\\n的场景</p>" +
"<p>匹配断前:用于有特殊帧头的场景</p>" +
"<p>固定字节断帧传输大量数据比如可以每隔1024字节断帧方便查看数据</p>",
value: "值",
timeout: "超时",
match: "匹配",
byte: "字节",
begin: "断",
end: "断",
other: "其他",
decodeAnsiEscapeCodes: "解码ANSI转义码",
ansiTooltips:
"<p>ANSI转义码对终端和文本有很多作用比如改变文本颜色等。</p>\n" +
"<p>\n" +
" 简单了解->\n" +
" <a target=\"_blank\" href=\"https://yunsi.studio/wireless-debugger/docs/uart-webhost/ansi-escape-code\">\n" +
" https://yunsi.studio/wireless-debugger/docs/uart-webhost/ansi-escape-code\n" +
" </a>\n" +
"</p>",
filter: "过滤",
textAndEscape: "文本,支持\\n\\x",
autoUpdateNewData: "新数据自动刷新",
updateFrequency: "数据显示刷新间隔(ms)",
updateFrequencyTooltip: "提高间隔可减少CPU资源的使用",
addHeader: "增加帧头",
addFooter: "增加帧尾",
passthrough: "透传",
proxy: "透传",
serverPort: "服务器端口",
connectedClient: "已连接的客户端",
refresh: "刷新",
interface: "接口",
noClientConnected: "无客户端连接",
import: "导入",
export: "导出",
reset: "重置",
resetTooltip: "刷新页面后生效",
saveToLocal: "保存到本地",
saveToLocalTooltip: "若存在多个页面,会相互覆盖",
add: "添加",
edit: "编辑",
drag: "拖拽",
ipChangeAlert: "IP地址改变会导致配置丢失",
layout: "布局",
landscape: "横/行",
portrait: "竖/列",
responsive: "自适应",
configPannel: "设置窗",
displayPannel: "数据窗",
macroPannel: "快捷窗",
autoScrollToBottom: "自动滚动到底部",
clearScreen: "清屏",
autoUpdate: "自动刷新",
tempDisplayTooltip: "未满足断帧规则的数据未超时暂时实时显示在此区域。超过8192字节自动断帧",
loopSend: "循环发送",
loopSendTooltip: "实际频率受界面刷新率影响,如需要更精确,可以尝试关闭‘自动刷新’",
sendFormat: "发送格式",
cachedFrame: "缓存帧数",
format: "格式化",
}
} }

View File

@ -364,18 +364,18 @@ export const useDataViewerStore = defineStore('text-viewer', () => {
const frameBreakSize = ref(0); const frameBreakSize = ref(0);
const frameBreakRules = ref([{ const frameBreakRules = ref([{
name: '超时(ms)', name: 'timeout',
type: 'number', type: 'number',
min: -1, min: -1,
draggable: false, draggable: false,
transformData: breakDelay, transformData: breakDelay,
}, { }, {
name: '匹配', name: 'match',
type: 'text', type: 'text',
draggable: true, draggable: true,
transformData: breakSequence, transformData: breakSequence,
}, { }, {
name: '字节(B)', name: 'byte',
type: 'number', type: 'number',
min: 0, min: 0,
draggable: true, draggable: true,
@ -437,9 +437,9 @@ export const useDataViewerStore = defineStore('text-viewer', () => {
function reloadFrameBreak() { function reloadFrameBreak() {
/* function and ref can not be stored in localStorage */ /* function and ref can not be stored in localStorage */
for (let i = 0; i < frameBreakRules.value.length; i++) { for (let i = 0; i < frameBreakRules.value.length; i++) {
if (frameBreakRules.value[i].name === "超时(ms)") { if (frameBreakRules.value[i].name === "timeout") {
frameBreakRules.value[i].transformData = breakDelay; frameBreakRules.value[i].transformData = breakDelay;
} else if (frameBreakRules.value[i].name === "匹配") { } else if (frameBreakRules.value[i].name === "match") {
frameBreakRules.value[i].transformData = breakSequence; frameBreakRules.value[i].transformData = breakSequence;
} else { } else {
frameBreakRules.value[i].transformData = breakSize; frameBreakRules.value[i].transformData = breakSize;

View File

@ -38,15 +38,24 @@
<div class="custom-style flex justify-center"> <div class="custom-style flex justify-center">
<el-segmented v-model="store.winLayoutMode" :options="layoutOptions" size="small"/> <el-segmented v-model="store.winLayoutMode" :options="layoutOptions" size="small"/>
</div> </div>
<el-checkbox label="自适应" v-model="store.winAutoLayout" border size="small" <el-checkbox v-model="store.winAutoLayout" border size="small"
:disabled="store.winLayoutMode==='col'"/> :disabled="store.winLayoutMode==='col'">
<el-checkbox label="设置窗" v-model="store.winLeft.show" border size="small" :disabled="store.winAutoLayout"/> {{ $t('uart.responsive') }}
<el-checkbox label="数据窗" v-model="winDataView.show" border size="small" :disabled="store.winAutoLayout"/> </el-checkbox>
<el-checkbox label="快捷窗" v-model="store.winRight.show" border size="small" :disabled="store.winAutoLayout"/> <el-checkbox v-model="store.winLeft.show" border size="small" :disabled="store.winAutoLayout">
{{ $t("uart.configPannel") }}
</el-checkbox>
<el-checkbox v-model="winDataView.show" border size="small" :disabled="store.winAutoLayout">
{{ $t('uart.displayPannel') }}
</el-checkbox>
<el-checkbox v-model="store.winRight.show" border size="small" :disabled="store.winAutoLayout">
{{ $t('uart.macroPannel') }}
</el-checkbox>
</div> </div>
<template #reference> <template #reference>
<el-button class="min-h-full" type="primary" :size="layoutConf.isMedium ? 'small' : 'default'">布局 <el-button class="min-h-full" type="primary" :size="layoutConf.isMedium ? 'small' : 'default'">
{{ $t('uart.layout') }}
</el-button> </el-button>
</template> </template>
</el-popover> </el-popover>
@ -56,7 +65,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {onMounted, onUnmounted, reactive, type Ref, ref, type UnwrapRef, watch} from "vue"; import {computed, onMounted, onUnmounted, reactive, type Ref, ref, type UnwrapRef, watch} from "vue";
import {breakpointsTailwind, useBreakpoints} from '@vueuse/core' import {breakpointsTailwind, useBreakpoints} from '@vueuse/core'
import {useDataViewerStore} from '@/stores/dataViewerStore'; import {useDataViewerStore} from '@/stores/dataViewerStore';
import * as api from '@/api'; import * as api from '@/api';
@ -83,6 +92,7 @@ import {isDevMode} from "@/composables/buildMode";
import {useWsStore} from "@/stores/websocket"; import {useWsStore} from "@/stores/websocket";
import {useUartStore} from "@/stores/useUartStore"; import {useUartStore} from "@/stores/useUartStore";
import TextDataMacro from "@/views/text-data-viewer/textDataMacro.vue"; import TextDataMacro from "@/views/text-data-viewer/textDataMacro.vue";
import {translate} from "@/locales";
const store = useDataViewerStore() const store = useDataViewerStore()
const wsStore = useWsStore() const wsStore = useWsStore()
@ -100,13 +110,13 @@ const layoutConf = reactive({
isMedium: breakpoints.smaller("lg"), isMedium: breakpoints.smaller("lg"),
}); });
const layoutOptions = [{ const layoutOptions = computed(() => [{
label: '横/行', label: translate("uart.landscape"),
value: 'row' value: 'row'
}, { }, {
label: '竖/列', label: translate("uart.portrait"),
value: 'col' value: 'col'
}] }]);
interface WinProperty { interface WinProperty {
show: boolean; show: boolean;

View File

@ -1,7 +1,7 @@
<template> <template>
<nav class="relative px-2 py-0.5 sm:py-1 flex justify-between items-center border-b h-full"> <nav class="relative px-2 py-0.5 sm:py-1 flex justify-between items-center border-b h-full">
<div class="flex"> <div class="flex">
<button @click.prevent="sideMenuOpen=true" class="flex items-center hover:text-blue-600 pl-1 mx-4"> <button @click.prevent="sideMenuOpen=true" class="flex items-center hover:text-blue-600 pl-1 mx-2 sm:mx-4">
<svg class="block h-3 lg:h-4 lg:w-4 fill-current" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> <svg class="block h-3 lg:h-4 lg:w-4 fill-current" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<title>导航侧栏</title> <title>导航侧栏</title>
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"></path> <path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"></path>
@ -20,7 +20,7 @@
<!-- <router-link to="/" class="flex items-center text-sm text-blue-600 font-bold">主页</router-link>--> <!-- <router-link to="/" class="flex items-center text-sm text-blue-600 font-bold">主页</router-link>-->
<!-- <a class="flex items-center text-sm text-blue-600 font-bold" href="/">主页6</a>--> <!-- <a class="flex items-center text-sm text-blue-600 font-bold" href="/">主页6</a>-->
<div class="flex pt-0.5 sm:pt-1 ml-4 text-sm items-center sm:hidden"> <div class="flex pt-0.5 sm:pt-1 ml-4 text-xs items-center sm:hidden">
<router-link :to="route.fullPath">{{ route.meta.title }}</router-link> <router-link :to="route.fullPath">{{ route.meta.title }}</router-link>
</div> </div>
</div> </div>
@ -36,6 +36,20 @@
<!-- <a class="md:ml-auto md:mr-3"></a>--> <!-- <a class="md:ml-auto md:mr-3"></a>-->
<div class="flex h-full"> <div class="flex h-full">
<div id="page-spec-slot" class="content-center h-full flex flex-row"></div> <div id="page-spec-slot" class="content-center h-full flex flex-row"></div>
<div class="mr-2">
<el-select v-model="language" class="min-w-20 h-full" @change="handleLanguageChange">
<el-option value="en">🇺🇸 English</el-option>
<el-option value="zh">🇨🇳 简体中文</el-option>
<el-option value="fr">🇫🇷 Français</el-option>
<template #label>
<div class="flex">
<InlineSvg name="translate" class="w-4 mr-1"></InlineSvg>
{{ languageFlag }}
</div>
</template>
</el-select>
</div>
<div class="lg:hidden"> <div class="lg:hidden">
<el-button :type="wsColor" size="small" class="transition duration-1000 min-h-full"> <el-button :type="wsColor" size="small" class="transition duration-1000 min-h-full">
<InlineSvg v-show="wsColor!=='success'" name="link-off" class="mr-2" width="20"></InlineSvg> <InlineSvg v-show="wsColor!=='success'" name="link-off" class="mr-2" width="20"></InlineSvg>
@ -81,9 +95,9 @@
<div> <div>
<el-button @click="toggle"> <el-button @click="toggle">
<InlineSvg v-if="!isFullscreen" name="open-in-full" width="16px" fill="#000000"></InlineSvg> <InlineSvg v-if="!isFullscreen" name="open-in-full" width="16px" fill="#000000"></InlineSvg>
<p v-if="!isFullscreen">全屏</p> <p v-if="!isFullscreen">{{ translate('page.fullscreen') }}</p>
<InlineSvg v-if="isFullscreen" name="close-fullscreen" width="16px" fill="#000000"></InlineSvg> <InlineSvg v-if="isFullscreen" name="close-fullscreen" width="16px" fill="#000000"></InlineSvg>
<p v-if="isFullscreen">缩小</p> <p v-if="isFullscreen">{{ translate('page.windowed') }}</p>
</el-button> </el-button>
</div> </div>
</template> </template>
@ -109,7 +123,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import InlineSvg from "@/components/InlineSvg.vue"; import InlineSvg from "@/components/InlineSvg.vue";
import {computed, type Ref, ref} from "vue"; import {computed, type ComputedRef, type Ref, ref} from "vue";
import {useWsStore} from "@/stores/websocket"; import {useWsStore} from "@/stores/websocket";
import {translate} from "@/locales"; import {translate} from "@/locales";
import {ControlEvent} from "@/api"; import {ControlEvent} from "@/api";
@ -117,11 +131,21 @@ import {useRoute} from "vue-router";
import { useFullscreen } from '@vueuse/core' import { useFullscreen } from '@vueuse/core'
import {useUpdateStore} from "@/stores/useUpdateStore"; import {useUpdateStore} from "@/stores/useUpdateStore";
import {isOTAEnabled} from "@/composables/buildMode"; import {isOTAEnabled} from "@/composables/buildMode";
import {getFlagFromLang, locale, setLang} from "@/i18n"
const wsStore = useWsStore(); const wsStore = useWsStore();
const updateStore = useUpdateStore(); const updateStore = useUpdateStore();
const {isFullscreen, toggle} = useFullscreen(); const {isFullscreen, toggle} = useFullscreen();
const route = useRoute(); const route = useRoute();
const language = ref(locale);
const languageFlag = computed(() => {
return getFlagFromLang(language.value);
});
function handleLanguageChange(lang: string) {
setLang(lang);
}
const sideMenuItemClass = "block p-4 text-sm font-semibold hover:bg-blue-50 hover:text-blue-600 rounded flex" const sideMenuItemClass = "block p-4 text-sm font-semibold hover:bg-blue-50 hover:text-blue-600 rounded flex"
const sideMenuOpen = ref(false); const sideMenuOpen = ref(false);
@ -144,7 +168,7 @@ const wsColor = computed(() => {
}); });
const wsState = computed(() => { const wsState = computed(() => {
return translate(wsStore.state); return translate(wsStore.state.toLocaleLowerCase());
}); });
type Item = { type Item = {
@ -154,7 +178,7 @@ type Item = {
badge?: Ref<boolean>; badge?: Ref<boolean>;
}; };
const menuItems: Item[] = ([ const menuItems: ComputedRef<Item[]> = computed(() => ([
{ {
name: translate("page.uart"), name: translate("page.uart"),
href: "/uart", href: "/uart",
@ -165,9 +189,10 @@ const menuItems: Item[] = ([
name: translate("page.feedback"), name: translate("page.feedback"),
href: "/feedback", href: "/feedback",
}, },
]); ]));
const sideBarItems: Item[] = ([ const sideBarItems: ComputedRef<Item[]> = computed(() => {
const items: Item[] = [
{ {
name: translate("page.uart"), name: translate("page.uart"),
href: "/uart", href: "/uart",
@ -181,15 +206,18 @@ const sideBarItems: Item[] = ([
name: translate("page.feedback"), name: translate("page.feedback"),
href: "/feedback", href: "/feedback",
}, },
]); ];
if (isOTAEnabled()) {
if (isOTAEnabled()) { items.push({
sideBarItems.push({
name: translate("page.update"), name: translate("page.update"),
href: "/update", href: "/update",
badge: computed(() => updateStore.canUpdate), badge: computed(() => updateStore.canUpdate),
}) })
} }
return items;
});
</script> </script>
@ -217,5 +245,9 @@ if (isOTAEnabled()) {
padding: 0; padding: 0;
} }
.el-select :deep(.el-select__wrapper) {
@apply h-full;
}
</style> </style>

View File

@ -1,14 +1,14 @@
<template> <template>
<div> <div>
<el-tabs v-model="store.configPanelTab" class="mx-2 custom-tabs fit"> <el-tabs v-model="store.configPanelTab" class="mx-2 custom-tabs fit">
<el-tab-pane label="接口" name="first" class="min-h-80"> <el-tab-pane name="first" class="min-h-80">
<template #label>{{ $t("uart.port") }}</template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<el-form :size="store.winLeft.show ? '' : 'small'"> <el-form :size="store.winLeft.show ? '' : 'small'">
<el-form-item <el-form-item
label="波特率"
class="mb-2" class="mb-2"
> >
<template #label>{{ $t("uart.baudrate") }}</template>
<div class="flex w-full"> <div class="flex w-full">
<el-select v-model="store.uartBaud" :teleported="false" @change="onUartBaudChange"> <el-select v-model="store.uartBaud" :teleported="false" @change="onUartBaudChange">
<template #header> <template #header>
@ -16,17 +16,17 @@
<div class="flex gap-0"> <div class="flex gap-0">
<el-input-number <el-input-number
v-model="uartCustomBaud" v-model="uartCustomBaud"
placeholder="自定义波特率" :placeholder="translate('uart.customBaud')"
size="small" size="small"
:controls="false" :controls="false"
:min="110" :min="110"
class="flex-grow" class="flex-grow"
></el-input-number> ></el-input-number>
<el-button size="small" @click="onUseCustomUartBaud">使用</el-button> <el-button size="small" @click="onUseCustomUartBaud">{{ $t('uart.use') }}</el-button>
<!-- <el-button size="small" @click="onConfirm" class="ml-0">增加</el-button>--> <!-- <el-button size="small" @click="onConfirm" class="ml-0">增加</el-button>-->
</div> </div>
<el-option-group label="常用"> <el-option-group :label="translate('uart.commonlyUsed')">
<el-option <el-option
v-for="item in store.predefinedUartBaudFrequent" v-for="item in store.predefinedUartBaudFrequent"
:key="item.baud" :key="item.baud"
@ -35,7 +35,7 @@
/> />
</el-option-group> </el-option-group>
<el-option-group label="其他"> <el-option-group :label="translate('uart.other')">
<el-option <el-option
v-for="item in store.uartBaudList" v-for="item in store.uartBaudList"
:key="item.baud" :key="item.baud"
@ -48,9 +48,9 @@
</el-select> </el-select>
</div> </div>
</el-form-item> </el-form-item>
<p class="text-xs">实际波特率:{{ store.uartBaudReal }}</p> <p class="text-xs">{{ $t('uart.actual') }} {{ $t('uart.baudrate') }}:{{ store.uartBaudReal }}</p>
<el-form-item label="数据位" class="mb-2"> <el-form-item :label="translate('uart.dataBits')" class="mb-2">
<el-select v-model="store.uartConfig.data_bits" :teleported="false" <el-select v-model="store.uartConfig.data_bits" :teleported="false"
placeholder="Select" @change="onUartConfigChange"> placeholder="Select" @change="onUartConfigChange">
<el-option <el-option
@ -62,7 +62,7 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="校验位" class="mb-2"> <el-form-item :label="translate('uart.parity')" class="mb-2">
<el-select v-model="store.uartConfig.parity" :teleported="false" <el-select v-model="store.uartConfig.parity" :teleported="false"
placeholder="Select" @change="onUartConfigChange"> placeholder="Select" @change="onUartConfigChange">
<el-option <el-option
@ -74,7 +74,7 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="停止位"> <el-form-item :label="translate('uart.stopBits')">
<el-select v-model="store.uartConfig.stop_bits" :teleported="false" <el-select v-model="store.uartConfig.stop_bits" :teleported="false"
placeholder="Select" @change="onUartConfigChange"> placeholder="Select" @change="onUartConfigChange">
<el-option <el-option
@ -92,7 +92,7 @@
:disabled="wsStore.state !== ControlEvent.CONNECTED" :disabled="wsStore.state !== ControlEvent.CONNECTED"
@click="store.acceptIncomingData = !store.acceptIncomingData" @click="store.acceptIncomingData = !store.acceptIncomingData"
> >
{{ store.acceptIncomingData ? "停止数据收发" : "开始数据收发" }} {{ store.acceptIncomingData ? $t("uart.stopCommunication") : $t("uart.startCommunication") }}
</el-button> </el-button>
</div> </div>
</el-form-item> </el-form-item>
@ -102,39 +102,40 @@
</el-tab-pane> </el-tab-pane>
<!-- ///////////////////////////////////////////////////////////////// --> <!-- ///////////////////////////////////////////////////////////////// -->
<el-tab-pane label="显示框" name="second"> <el-tab-pane name="second">
<template #label>{{ $t("uart.displayPannel") }}</template>
<div class="flex flex-col"> <div class="flex flex-col">
<el-collapse v-model="collapseActiveName"> <el-collapse v-model="collapseActiveName">
<el-collapse-item name="1"> <el-collapse-item name="1">
<template #title> <template #title>
显示选项 {{ $t('uart.displayOptions') }}
</template> </template>
<template #default> <template #default>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex flex-col"> <div class="flex flex-col">
<el-checkbox border v-model="store.showText" label="显示文本"/> <el-checkbox border v-model="store.showText" :label="translate('uart.text')"/>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<el-checkbox border v-model="store.showHex" label="显示HEX"/> <el-checkbox border v-model="store.showHex" label="HEX"/>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<el-checkbox border v-model="store.showHexdump" label="显示HEXDUMP"/> <el-checkbox border v-model="store.showHexdump" label="HEXDUMP"/>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<el-checkbox border v-model="store.showTimestamp">显示时间戳</el-checkbox> <el-checkbox border v-model="store.showTimestamp" :label="translate('uart.timestamp')"/>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<el-checkbox border v-model="store.enableLineWrap" label="启用换行"/> <el-checkbox border v-model="store.enableLineWrap" :label="translate('uart.lineWrap')"/>
</div> </div>
<el-tag type="success"> <el-tag type="success">
<el-text type="success">RX HEXDUMP高亮选色</el-text> <el-text type="success">RX HEXDUMP {{ $t("uart.highlight") }}</el-text>
<el-color-picker v-model="store.RxHexdumpColor" show-alpha :predefine="store.predefineColors" <el-color-picker v-model="store.RxHexdumpColor" show-alpha :predefine="store.predefineColors"
size="small"/> size="small"/>
</el-tag> </el-tag>
<el-tag type="primary"> <el-tag type="primary">
<el-text type="primary">TX HEXDUMP高亮选色</el-text> <el-text type="primary">TX HEXDUMP {{ $t("uart.highlight") }}</el-text>
<el-color-picker v-model="store.TxHexdumpColor" show-alpha :predefine="store.predefineColors" <el-color-picker v-model="store.TxHexdumpColor" show-alpha :predefine="store.predefineColors"
size="small"/> size="small"/>
</el-tag> </el-tag>
@ -142,33 +143,26 @@
</template> </template>
</el-collapse-item> </el-collapse-item>
<el-collapse-item name="2"> <el-collapse-item name="2" :title="translate('uart.frameBreakStrategy')">
<template #title>
断帧策略
</template>
<VueDraggable v-model="store.frameBreakRules" target="tbody" handle=".sort-target" <VueDraggable v-model="store.frameBreakRules" target="tbody" handle=".sort-target"
:animation="150" :animation="150"
:on-move="checkMove"> :on-move="checkMove">
<table class="w-full bg-white"> <table class="w-full bg-white">
<thead> <thead>
<tr class="text-sm h-7"> <tr class="text-sm h-7">
<th>优先级</th> <th>{{ $t('uart.priority') }}</th>
<th> <th>
<div class="flex justify-center"> <div class="flex justify-center">
规则 {{ translate('uart.rule' as TranslationKeys) }}
<el-tooltip placement="top" effect="light"> <el-tooltip placement="top" effect="light">
<template #content> <template #content>
<p>超时=-1 禁用超时断帧</p> <div v-html="translate('uart.ruleTips')"></div>
<p>超时=0 当机立断收到任何数据都视为完整数据</p>
<p>匹配断后典型\n的场景</p>
<p>匹配断前用于有特殊帧头的场景</p>
<p>固定字节断帧传输大量数据比如可以每隔1024字节断帧方便查看数据</p>
</template> </template>
<InlineSvg name="help" class="w-4 text-gray-500 cursor-help"></InlineSvg> <InlineSvg name="help" class="w-4 text-gray-500 cursor-help"></InlineSvg>
</el-tooltip> </el-tooltip>
</div> </div>
</th> </th>
<th></th> <th>{{ translate('uart.value' as TranslationKeys) }}</th>
</tr> </tr>
</thead> </thead>
<tbody class="text-xs text-center"> <tbody class="text-xs text-center">
@ -176,20 +170,22 @@
<td :class="item.draggable ? 'sort-target' : ''"> <td :class="item.draggable ? 'sort-target' : ''">
{{ item.draggable ? index : 'NaN' }} {{ item.draggable ? index : 'NaN' }}
</td> </td>
<td :class="item.draggable ? 'sort-target' : ''">{{ item.name }}</td> <td :class="item.draggable ? 'sort-target' : ''">
{{ translate("uart." + item.name) }}
</td>
<td> <td>
<div v-if="item.type === 'number'"> <div v-if="item.type === 'number'">
<el-input-number v-if="item.name === '超时(ms)'" v-model="store.frameBreakDelay" :min="item.min || 0" size="small" style="width: 100px"/> <el-input-number v-if="item.name === 'timeout'" v-model="store.frameBreakDelay" :min="item.min || 0" size="small" style="width: 100px"/>
<el-input-number v-else v-model="store.frameBreakSize" :min="item.min || 0" size="small" style="width: 100px"/> <el-input-number v-else v-model="store.frameBreakSize" :min="item.min || 0" size="small" style="width: 100px"/>
</div> </div>
<div v-else> <div v-else>
<el-input class="break-input" v-model="store.frameBreakSequence" placeholder="文本;支持\n\x" size="small" <el-input class="break-input" v-model="store.frameBreakSequence" :placeholder="translate('uart.textAndEscape')" size="small"
style="width: 100px"> style="width: 100px">
<template #prepend> <template #prepend>
<el-button size="small" @click="store.frameBreakAfterSequence = false"> <el-button size="small" @click="store.frameBreakAfterSequence = false">
<span <span
:class="store.frameBreakAfterSequence ? 'text-gray-400' : 'text-blue-400 font-bold'"> :class="store.frameBreakAfterSequence ? 'text-gray-400' : 'text-blue-400 font-bold'">
{{ translate("uart.begin") }}
</span> </span>
</el-button> </el-button>
</template> </template>
@ -197,7 +193,7 @@
<el-button size="small" @click="store.frameBreakAfterSequence = true"> <el-button size="small" @click="store.frameBreakAfterSequence = true">
<span <span
:class="store.frameBreakAfterSequence ? 'text-blue-400 font-bold' : 'text-gray-300'"> :class="store.frameBreakAfterSequence ? 'text-blue-400 font-bold' : 'text-gray-300'">
{{ translate("uart.end") }}
</span> </span>
</el-button> </el-button>
</template> </template>
@ -210,10 +206,7 @@
</VueDraggable> </VueDraggable>
</el-collapse-item> </el-collapse-item>
<el-collapse-item name="3"> <el-collapse-item name="3" :title="translate('uart.other')">
<template #title>
其他
</template>
<template #default> <template #default>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<el-tooltip <el-tooltip
@ -222,30 +215,24 @@
placement="right-start" placement="right-start"
> >
<template #content> <template #content>
<p>ANSI转义码对终端和文本有很多作用比如改变文本颜色等</p> <div v-html="translate('uart.ansiTooltips')"></div>
<p>
简单了解->
<el-link target="_blank" href="https://zhuanlan.zhihu.com/p/390666800">
https://zhuanlan.zhihu.com/p/390666800
</el-link>
</p>
</template> </template>
<el-checkbox border v-model="store.enableAnsiDecode">解析ANSI转义码</el-checkbox> <el-checkbox border v-model="store.enableAnsiDecode">{{ translate('uart.decodeAnsiEscapeCodes') }}</el-checkbox>
</el-tooltip> </el-tooltip>
<el-input v-model="store.filterValue" placeholder="文本;支持\n\x" clearable> <el-input v-model="store.filterValue" :placeholder="translate('uart.textAndEscape')" clearable>
<template #prepend> <template #prepend>
过滤 {{ translate("uart.filter") }}
</template> </template>
</el-input> </el-input>
<div class="border rounded flex flex-col"> <div class="border rounded flex flex-col">
<el-checkbox border v-model="store.dataFilterAutoUpdate">新数据自动刷新</el-checkbox> <el-checkbox border v-model="store.dataFilterAutoUpdate">{{ translate('uart.autoUpdateNewData') }}</el-checkbox>
<el-tooltip content="提高间隔可减少CPU资源的使用" placement="right" effect="light" <el-tooltip :content="translate('uart.updateFrequencyTooltip')" placement="right" effect="light"
:show-after="500"> :show-after="500">
<div class="flex gap-4 p-2"> <div class="flex gap-4 p-2">
<el-text>数据显示刷新间隔(ms)</el-text> <el-text>{{ translate('uart.updateFrequency') }}</el-text>
<el-input-number <el-input-number
:step="10" :step="10"
:min="10" :min="10"
@ -300,34 +287,36 @@
</el-tab-pane> </el-tab-pane>
<!-- ///////////////////////////////////////////////////////////// --> <!-- ///////////////////////////////////////////////////////////// -->
<el-tab-pane label="发送" name="third"> <el-tab-pane :label="translate('uart.send')" name="third">
<template #label>{{ $t("uart.send") }}</template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<el-input v-model="store.textPrefixValue" placeholder="支持\n\x" clearable> <el-input v-model="store.textPrefixValue" :placeholder="translate('uart.textAndEscape')" clearable>
<template #prepend> <template #prepend>
{{ `添加帧头►` }} {{ translate('uart.addHeader') }}
</template> </template>
</el-input> </el-input>
<el-input v-model="store.textSuffixValue" placeholder="支持\n\x" clearable> <el-input v-model="store.textSuffixValue" :placeholder="translate('uart.textAndEscape')" clearable>
<template #append> <template #append>
添加帧尾 {{ translate('uart.addFooter') }}
</template> </template>
</el-input> </el-input>
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="透传" name="fourth" class="min-h-80"> <el-tab-pane :label="translate('uart.proxy')" name="fourth" class="min-h-80">
<template #label>{{ $t("uart.passthrough") }}</template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="border rounded bg-white p-2"> <div class="border rounded bg-white p-2">
<span class="border-r px-2">TCP服务器端口</span> <span class="border-r px-2">TCP {{ translate('uart.serverPort') }}</span>
<span class="px-2 cursor-not-allowed">1346</span> <span class="px-2 cursor-not-allowed">1346</span>
</div> </div>
<div> <div>
<p><el-button @click="refreshTCPClientList" size="small" type="primary" :plain="true">刷新</el-button> </p> <p><el-button @click="refreshTCPClientList" size="small" type="primary" :plain="true">{{ translate('uart.refresh') }}</el-button> {{ translate('uart.connectedClient') }}</p>
<el-table :data="dfStore.instanceList.filter((item) => (item.port_info as ISocketInfo).local_port === 1346)" empty-text="无客户端连接"> <el-table :data="dfStore.instanceList.filter((item) => (item.port_info as ISocketInfo).local_port === 1346)" :empty-text="translate('uart.noClientConnected')">
<el-table-column label="IP" prop="port_info.foreign_ip" /> <el-table-column label="IP" prop="port_info.foreign_ip" />
<el-table-column label="端口" prop="port_info.foreign_port"/> <el-table-column :label="translate('uart.port')" prop="port_info.foreign_port"/>
</el-table> </el-table>
</div> </div>
</div> </div>
@ -338,7 +327,7 @@
<script setup lang="ts"> <script setup lang="ts">
import {VueDraggable} from 'vue-draggable-plus' import {VueDraggable} from 'vue-draggable-plus'
import {ref} from "vue"; import {computed, ref} from "vue";
import {useDataViewerStore} from "@/stores/dataViewerStore"; import {useDataViewerStore} from "@/stores/dataViewerStore";
import {useWsStore} from "@/stores/websocket"; import {useWsStore} from "@/stores/websocket";
import {globalNotify} from "@/composables/notification"; import {globalNotify} from "@/composables/notification";
@ -349,6 +338,7 @@ import {useDataFlowStore} from "@/stores/useDataFlowStore";
import {wt_data_flow_get_instance_list, type ISocketInfo} from "@/api/apiDataFlow"; import {wt_data_flow_get_instance_list, type ISocketInfo} from "@/api/apiDataFlow";
import {uart_set_baud, uart_set_config} from "@/api/apiUart"; import {uart_set_baud, uart_set_config} from "@/api/apiUart";
import {useUartStore} from "@/stores/useUartStore"; import {useUartStore} from "@/stores/useUartStore";
import {translate, type TranslationKeys} from "@/locales";
const store = useDataViewerStore() const store = useDataViewerStore()
const uartStore = useUartStore() const uartStore = useUartStore()
@ -375,18 +365,18 @@ const uartDataBitsOptions = [
} }
] ]
const uartParityOptions = [ const uartParityOptions = computed(() => [
{ {
key: 0, key: 0,
label: "无none", label: translate("uart.parityNone"),
}, { }, {
key: 1, key: 1,
label: "奇odd", label: translate("uart.parityOdd"),
}, { }, {
key: 2, key: 2,
label: "偶even", label: translate("uart.parityEven"),
} }
] ]);
const uartStopBitsOptions = [ const uartStopBitsOptions = [

View File

@ -1,25 +1,27 @@
<template> <template>
<div class="flex items-center mb-2 flex-wrap gap-2"> <div class="flex items-center mb-2 flex-wrap gap-2">
<el-button type="primary" @click="importSettings">导入</el-button> <el-button type="primary" @click="importSettings">{{ translate('uart.import') }}</el-button>
<el-button type="warning" @click="exportSettings">导出</el-button> <el-button type="warning" @click="exportSettings">{{ translate('uart.export') }}</el-button>
<el-tooltip <el-tooltip
effect="light" effect="light"
placement="top" placement="top"
:show-after="500"
> >
<template #content> <template #content>
<p>刷新页面后生效</p> <p>{{ translate('uart.resetTooltip') }}</p>
</template> </template>
<el-button type="info" @click="resetSettings">重置</el-button> <el-button type="info" @click="resetSettings">{{ translate('uart.reset') }}</el-button>
</el-tooltip> </el-tooltip>
<el-tooltip <el-tooltip
effect="light" effect="light"
placement="top" placement="top"
:show-after="500"
> >
<template #content> <template #content>
<p>若存在多个页面会相互覆盖</p> <p>{{ translate('uart.saveToLocalTooltip') }}</p>
</template> </template>
<el-checkbox border v-model="store.autoSaveSettings">保存至本地</el-checkbox> <el-checkbox border v-model="store.autoSaveSettings">{{ translate('uart.saveToLocal') }}</el-checkbox>
</el-tooltip> </el-tooltip>
</div> </div>
@ -27,17 +29,17 @@
<el-button type="primary" @click="() => { <el-button type="primary" @click="() => {
store.macroData.push({ store.macroData.push({
value: '', value: '',
label: '发送', label: translate('uart.send'),
id: store.macroId, id: store.macroId,
}) })
store.macroId++; store.macroId++;
}">添加 }">{{ translate('uart.add') }}
</el-button> </el-button>
<el-checkbox v-model="editMode" border>编辑</el-checkbox> <el-checkbox v-model="editMode" border>{{ translate('uart.edit') }}</el-checkbox>
<el-checkbox v-model="draggableEnabled" border>拖拽</el-checkbox> <el-checkbox v-model="draggableEnabled" border>{{ translate('uart.drag') }}</el-checkbox>
</div> </div>
<div> <div>
<el-alert v-if="store.ipChangeAlert" @close="store.ipChangeAlert=false">IP地址改变会导致配置丢失</el-alert> <el-alert v-if="store.ipChangeAlert" @close="store.ipChangeAlert=false">{{ translate('uart.ipChangeAlert') }}</el-alert>
</div> </div>
<VueDraggable v-model="store.macroData" handle=".sort-target" <VueDraggable v-model="store.macroData" handle=".sort-target"
@ -67,6 +69,7 @@ import {VueDraggable} from "vue-draggable-plus";
import {onMounted, ref} from "vue"; import {onMounted, ref} from "vue";
import {globalNotify, globalNotifyRightSide} from "@/composables/notification"; import {globalNotify, globalNotifyRightSide} from "@/composables/notification";
import {useDataViewerStore} from "@/stores/dataViewerStore"; import {useDataViewerStore} from "@/stores/dataViewerStore";
import {translate} from "../../locales";
const editMode = ref(false); const editMode = ref(false);
const draggableEnabled = ref(true); const draggableEnabled = ref(true);

View File

@ -18,18 +18,18 @@
</el-popover> </el-popover>
<div class="flex"> <div class="flex">
<el-checkbox size="small" v-model="store.forceToBottom" label="自动滚动至底部" border/> <el-checkbox size="small" v-model="store.forceToBottom" :label="translate('uart.autoScrollToBottom')" border/>
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="light" effect="light"
placement="top" placement="top"
> >
<template #content> <template #content>
<p>仅清除显示区域可用刷新恢复</p> <p>{{ translate('uart.clearTooltip') }}</p>
</template> </template>
<el-button size="small" @click="store.clearFilteredBuff"> <el-button size="small" @click="store.clearFilteredBuff">
<InlineSvg class="h-5" name="trash"></InlineSvg> <InlineSvg class="h-5" name="trash"></InlineSvg>
清屏 {{ $t('uart.clearScreen') }}
</el-button> </el-button>
</el-tooltip> </el-tooltip>
@ -39,10 +39,10 @@
placement="top" placement="top"
> >
<template #content> <template #content>
<p>与缓存同步+过滤</p> <p>{{ translate('uart.clearTooltip') }}</p>
</template> </template>
<el-button size="small" @click="store.refreshFilteredBuff"> <el-button size="small" @click="store.refreshFilteredBuff">
刷新 {{ $t('page.update') }}
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-tooltip <el-tooltip
@ -51,10 +51,10 @@
placement="top" placement="top"
> >
<template #content> <template #content>
<p>仅停止刷新显示区后台继续接收数据</p> <p>{{ translate('uart.autoUpdateTooltip') }}</p>
</template> </template>
<el-checkbox size="small" border v-model="store.dataFilterAutoUpdate"> <el-checkbox size="small" border v-model="store.dataFilterAutoUpdate">
自动刷新 {{ $t('uart.autoUpdate') }}
</el-checkbox> </el-checkbox>
</el-tooltip> </el-tooltip>
</div> </div>
@ -77,7 +77,7 @@
<p class="text-nowrap text-sm text-sky-500" v-else-if="item.type === 0" type="primary" v-show="store.showTimestamp"> <p class="text-nowrap text-sm text-sky-500" v-else-if="item.type === 0" type="primary" v-show="store.showTimestamp">
<span>{{ item.time }}</span>TX-|</p> <span>{{ item.time }}</span>TX-|</p>
<p class="text-nowrap text-sm text-amber-800" v-else type="primary" v-show="store.showTimestamp"> <p class="text-nowrap text-sm text-amber-800" v-else type="primary" v-show="store.showTimestamp">
<span>{{ item.time }}</span>未发送|</p> <span>{{ item.time }}</span>NS-|</p>
<p v-show="store.showText" <p v-show="store.showText"
v-html="item.str"></p> v-html="item.str"></p>
@ -111,7 +111,7 @@
<p class="text-nowrap text-sm text-sky-500" v-else-if="item.type === 0" type="primary" v-show="store.showTimestamp"> <p class="text-nowrap text-sm text-sky-500" v-else-if="item.type === 0" type="primary" v-show="store.showTimestamp">
<span>{{ item.time }}</span>TX-|</p> <span>{{ item.time }}</span>TX-|</p>
<p class="text-nowrap text-sm text-amber-800" v-else type="primary" v-show="store.showTimestamp"> <p class="text-nowrap text-sm text-amber-800" v-else type="primary" v-show="store.showTimestamp">
<span>{{ item.time }}</span>未发送|</p> <span>{{ item.time }}</span>NS-|</p>
<p v-show="store.showText" <p v-show="store.showText"
v-html="item.str"></p> v-html="item.str"></p>
</div> </div>
@ -132,7 +132,7 @@
<div class="shrink-0 flex h-8 mt-0.5 text-xs"> <div class="shrink-0 flex h-8 mt-0.5 text-xs">
<div class="flex shrink-0"> <div class="flex shrink-0">
<el-tooltip content="未满足断帧规则的数据未超时暂时实时显示在此区域。超过8192字节自动断帧" effect="light"> <el-tooltip :content="translate('uart.tempDisplayTooltip')" effect="light">
<InlineSvg name="help" class="w-3.5 h-3.5 text-gray-500 cursor-help"></InlineSvg> <InlineSvg name="help" class="w-3.5 h-3.5 text-gray-500 cursor-help"></InlineSvg>
</el-tooltip> </el-tooltip>
<p></p> <p></p>
@ -153,10 +153,10 @@
</el-tag> </el-tag>
</el-link> </el-link>
<el-tooltip content="实际频率受界面刷新率影响,如需要更精确,可以尝试关闭‘自动刷新’" placement="right" effect="light" :show-after="1000"> <el-tooltip :content="translate('uart.loopSendTooltip')" placement="right" effect="light" :show-after="1000">
<div class="flex align-center"> <div class="flex align-center">
<el-checkbox v-model="store.enableLoopSend" class="font-mono font-bold max-h-5" size="small" border> <el-checkbox v-model="store.enableLoopSend" class="font-mono font-bold max-h-5" size="small" border>
循环发送(ms) {{ translate('uart.loopSend') }}(ms)
</el-checkbox> </el-checkbox>
<el-input-number <el-input-number
v-model="store.loopSendFreq" v-model="store.loopSendFreq"
@ -170,7 +170,7 @@
</el-tooltip> </el-tooltip>
<el-link @click="store.isSendTextFormat = !store.isSendTextFormat"> <el-link @click="store.isSendTextFormat = !store.isSendTextFormat">
<el-tag class="font-mono font-bold" size="small">发送格式{{ store.isSendTextFormat ? "文本" : "HEX" }}</el-tag> <el-tag class="font-mono font-bold" size="small">{{ translate('uart.sendFormat') }}{{ store.isSendTextFormat ? translate("uart.text") : "HEX" }}</el-tag>
</el-link> </el-link>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@ -189,7 +189,7 @@
<el-link class="flex" @click="store.clearDataBuff" type="warning"> <el-link class="flex" @click="store.clearDataBuff" type="warning">
<InlineSvg class="h-5" name="trash"></InlineSvg> <InlineSvg class="h-5" name="trash"></InlineSvg>
</el-link> </el-link>
<span class="align-text-bottom">缓存帧数: {{ store.dataBufLength }}/30000</span> <span class="align-text-bottom">{{ translate('uart.cachedFrame') }}: {{ store.dataBufLength }}/30000</span>
</el-tag> </el-tag>
</div> </div>
</div> </div>
@ -197,14 +197,14 @@
<div class="flex flex-row font-mono"> <div class="flex flex-row font-mono">
<el-input type="textarea" :autosize="{ minRows: 1, maxRows: 6}" v-model="store.uartInputTextBox" clearable <el-input type="textarea" :autosize="{ minRows: 1, maxRows: 6}" v-model="store.uartInputTextBox" clearable
:placeholder="store.isSendTextFormat ? :placeholder="store.isSendTextFormat ?
'输入文本,支持\\n\\x转义' : translate('uart.textAndEscape') :
'输入HEX格式'" 'HEX'"
@keydown="handleTextboxKeydown" @keydown="handleTextboxKeydown"
></el-input> ></el-input>
<el-tooltip content="Ctrl+回车" placement="top" :auto-close="500"> <el-tooltip content="Ctrl+Enter" placement="top" :auto-close="500">
<el-button type="primary" <el-button type="primary"
@click="onSendClick"> @click="onSendClick">
{{ (store.isSendTextFormat || store.isHexStringValid) ? "发送" : "格式化" }} {{ (store.isSendTextFormat || store.isHexStringValid) ? translate("uart.send") : translate("格式化") }}
</el-button> </el-button>
</el-tooltip> </el-tooltip>
</div> </div>
@ -217,6 +217,7 @@ import InlineSvg from "@/components/InlineSvg.vue";
import TextDataConfig from "@/views/text-data-viewer/textDataConfig.vue"; import TextDataConfig from "@/views/text-data-viewer/textDataConfig.vue";
import {debouncedWatch} from "@vueuse/core"; import {debouncedWatch} from "@vueuse/core";
import {globalNotify} from "@/composables/notification"; import {globalNotify} from "@/composables/notification";
import {translate} from "../../locales";
const count = ref(0); const count = ref(0);
const vuetifyVirtualScrollBarRef = ref(document.body); const vuetifyVirtualScrollBarRef = ref(document.body);