Changes
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1,439 @@
|
||||
;(function (window, $) {
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
|
||||
const commandWords = [
|
||||
"打开",
|
||||
"进入",
|
||||
"跳转",
|
||||
"跳到",
|
||||
"去",
|
||||
"访问",
|
||||
"帮我",
|
||||
"请",
|
||||
"页面",
|
||||
"菜单",
|
||||
"一下"
|
||||
]
|
||||
|
||||
const state = {
|
||||
recognition: null,
|
||||
listening: false,
|
||||
menus: null,
|
||||
parentMap: {},
|
||||
lastText: "",
|
||||
pendingMatches: [],
|
||||
awaitingChoice: false,
|
||||
afterRecognitionEnd: null
|
||||
}
|
||||
|
||||
function normalizePlain(text) {
|
||||
return String(text || "")
|
||||
.toLowerCase()
|
||||
.replace(/[,。!?、,.!?;;::\s]/g, "")
|
||||
}
|
||||
|
||||
function normalizeCommand(text) {
|
||||
return normalizePlain(text)
|
||||
.replace(new RegExp(commandWords.join("|"), "g"), "")
|
||||
}
|
||||
|
||||
function notify(message, type) {
|
||||
if (window.ELEMENT && ELEMENT.Message) {
|
||||
ELEMENT.Message({message, type: type || "info"})
|
||||
return
|
||||
}
|
||||
window.alert(message)
|
||||
}
|
||||
|
||||
function speak(message, onEnd) {
|
||||
if (!window.speechSynthesis || !window.SpeechSynthesisUtterance) {
|
||||
if (typeof onEnd === "function") {
|
||||
onEnd()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
window.speechSynthesis.cancel()
|
||||
const utterance = new SpeechSynthesisUtterance(message)
|
||||
let ended = false
|
||||
function finish() {
|
||||
if (ended) {
|
||||
return
|
||||
}
|
||||
ended = true
|
||||
if (typeof onEnd === "function") {
|
||||
onEnd()
|
||||
}
|
||||
}
|
||||
|
||||
utterance.lang = "zh-CN"
|
||||
utterance.rate = 1
|
||||
utterance.volume = 1
|
||||
utterance.onend = function () {
|
||||
finish()
|
||||
}
|
||||
utterance.onerror = function () {
|
||||
finish()
|
||||
}
|
||||
window.speechSynthesis.speak(utterance)
|
||||
setTimeout(finish, Math.max(2500, message.length * 220))
|
||||
}
|
||||
|
||||
function runAfterRecognitionEnd(callback) {
|
||||
state.afterRecognitionEnd = callback
|
||||
if (!state.listening && typeof state.afterRecognitionEnd === "function") {
|
||||
const next = state.afterRecognitionEnd
|
||||
state.afterRecognitionEnd = null
|
||||
setTimeout(next, 150)
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
}
|
||||
|
||||
function flattenMenus(menus, parentId, result) {
|
||||
result = result || []
|
||||
;(menus || []).forEach(function (menu) {
|
||||
const id = menu.id
|
||||
const realParentId = menu.parentId || parentId || ""
|
||||
if (id) {
|
||||
state.parentMap[id] = realParentId
|
||||
}
|
||||
result.push(menu)
|
||||
if (menu.children && menu.children.length) {
|
||||
flattenMenus(menu.children, id, result)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
function getStoreMenus() {
|
||||
try {
|
||||
return window.store && window.store.state && window.store.state.user && window.store.state.user.menus
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getSessionMenus() {
|
||||
try {
|
||||
return JSON.parse(window.sessionStorage.getItem("zhgh_sub_app_menus") || "[]")
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function getMenus() {
|
||||
const cached = state.menus
|
||||
if (cached && cached.length) {
|
||||
return $.Deferred().resolve(cached).promise()
|
||||
}
|
||||
|
||||
const storeMenus = getStoreMenus()
|
||||
if (storeMenus && storeMenus.length) {
|
||||
state.menus = flattenMenus(storeMenus, "", []).filter(function (menu) {
|
||||
return menu.href
|
||||
})
|
||||
return $.Deferred().resolve(state.menus).promise()
|
||||
}
|
||||
|
||||
return $.get("/platform/sys/user/getLogonUser").then(function (res) {
|
||||
if (res && res.code === 0 && res.data && res.data.menus) {
|
||||
state.parentMap = {}
|
||||
state.menus = flattenMenus(res.data.menus, "", []).filter(function (menu) {
|
||||
return menu.href
|
||||
})
|
||||
return state.menus
|
||||
}
|
||||
const sessionMenus = getSessionMenus()
|
||||
if (sessionMenus && sessionMenus.length) {
|
||||
state.menus = flattenMenus(sessionMenus, "", []).filter(function (menu) {
|
||||
return menu.href
|
||||
})
|
||||
return state.menus
|
||||
}
|
||||
return []
|
||||
}, function () {
|
||||
const sessionMenus = getSessionMenus()
|
||||
if (sessionMenus && sessionMenus.length) {
|
||||
state.menus = flattenMenus(sessionMenus, "", []).filter(function (menu) {
|
||||
return menu.href
|
||||
})
|
||||
return state.menus
|
||||
}
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
function scoreMenu(menu, rawText) {
|
||||
const query = normalizeCommand(rawText)
|
||||
const rawQuery = normalizePlain(rawText)
|
||||
const name = normalizePlain(menu.name)
|
||||
const aliasName = normalizePlain(menu.aliasName)
|
||||
const href = normalizePlain(menu.href)
|
||||
const permission = normalizePlain(menu.permission)
|
||||
|
||||
if (!query || !name) {
|
||||
return 0
|
||||
}
|
||||
if (query === name || rawQuery === name || query === aliasName || rawQuery === aliasName) {
|
||||
return 100
|
||||
}
|
||||
if (name.indexOf(query) > -1 || name.indexOf(rawQuery) > -1 || aliasName.indexOf(query) > -1 || aliasName.indexOf(rawQuery) > -1) {
|
||||
return 80
|
||||
}
|
||||
if (query.indexOf(name) > -1 || rawQuery.indexOf(name) > -1 || (aliasName && (query.indexOf(aliasName) > -1 || rawQuery.indexOf(aliasName) > -1))) {
|
||||
return 70
|
||||
}
|
||||
if (href.indexOf(query) > -1 || permission.indexOf(query) > -1) {
|
||||
return 45
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function matchMenus(text, menus) {
|
||||
return (menus || [])
|
||||
.map(function (menu) {
|
||||
return {
|
||||
menu,
|
||||
score: scoreMenu(menu, text)
|
||||
}
|
||||
})
|
||||
.filter(function (item) {
|
||||
return item.score > 0
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
if (b.score !== a.score) {
|
||||
return b.score - a.score
|
||||
}
|
||||
return String(a.menu.name || "").length - String(b.menu.name || "").length
|
||||
})
|
||||
.slice(0, 5)
|
||||
}
|
||||
|
||||
function getRootMenuId(menu) {
|
||||
let id = menu.id
|
||||
let parentId = state.parentMap[id] || menu.parentId
|
||||
while (parentId) {
|
||||
id = parentId
|
||||
parentId = state.parentMap[id]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
function getCurrentSubAppId() {
|
||||
try {
|
||||
const app = JSON.parse(window.sessionStorage.getItem("zhgh_sub_app") || "{}")
|
||||
return app.id
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function openMenu(menu) {
|
||||
state.pendingMatches = []
|
||||
state.awaitingChoice = false
|
||||
if (!menu || !menu.href) {
|
||||
notify("该菜单没有配置可打开的地址", "warning")
|
||||
return
|
||||
}
|
||||
|
||||
const targetRootId = getRootMenuId(menu)
|
||||
const currentSubAppId = getCurrentSubAppId()
|
||||
const hasSubAppContainer = $("#sub-app-container-main-content-body").length > 0
|
||||
|
||||
if (hasSubAppContainer && currentSubAppId && currentSubAppId === targetRootId && typeof commonUtil !== "undefined" && commonUtil.pjaxPush) {
|
||||
commonUtil.pjaxPush(menu.href)
|
||||
return
|
||||
}
|
||||
window.location.href = menu.href
|
||||
}
|
||||
|
||||
function showCandidateMessage(matches) {
|
||||
const items = matches
|
||||
.map(function (item, index) {
|
||||
const href = item.menu.href ? " <span style=\"color:#909399\">" + escapeHtml(item.menu.href) + "</span>" : ""
|
||||
return "<p style=\"margin:6px 0\">" + (index + 1) + ". " + escapeHtml(item.menu.name) + href + "</p>"
|
||||
})
|
||||
.join("")
|
||||
|
||||
if (window.ELEMENT && ELEMENT.MessageBox) {
|
||||
ELEMENT.MessageBox.alert(items, "找到多个菜单,请说“打开第几个”", {
|
||||
dangerouslyUseHTMLString: true,
|
||||
confirmButtonText: "知道了",
|
||||
type: "info",
|
||||
callback: function () {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getChoiceIndex(text) {
|
||||
const normalized = normalizePlain(text)
|
||||
const numberMap = {
|
||||
"1": 0,
|
||||
"一": 0,
|
||||
"壹": 0,
|
||||
"幺": 0,
|
||||
"2": 1,
|
||||
"二": 1,
|
||||
"两": 1,
|
||||
"贰": 1,
|
||||
"3": 2,
|
||||
"三": 2,
|
||||
"叁": 2,
|
||||
"4": 3,
|
||||
"四": 3,
|
||||
"肆": 3,
|
||||
"5": 4,
|
||||
"五": 4,
|
||||
"伍": 4
|
||||
}
|
||||
|
||||
const digitMatch = normalized.match(/第?([1-5])个?/)
|
||||
if (digitMatch) {
|
||||
return numberMap[digitMatch[1]]
|
||||
}
|
||||
|
||||
const chineseMatch = normalized.match(/第?([一二两三四五壹贰叁肆伍幺])个?/)
|
||||
if (chineseMatch) {
|
||||
return numberMap[chineseMatch[1]]
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function handleChoice(text) {
|
||||
const index = getChoiceIndex(text)
|
||||
const matches = state.pendingMatches || []
|
||||
if (index >= 0 && index < matches.length) {
|
||||
if (window.ELEMENT && ELEMENT.MessageBox && typeof ELEMENT.MessageBox.close === "function") {
|
||||
ELEMENT.MessageBox.close()
|
||||
}
|
||||
openMenu(matches[index].menu)
|
||||
return
|
||||
}
|
||||
|
||||
notify("没有识别到有效序号,请说打开第几个", "warning")
|
||||
speak("没有识别到有效序号,请说打开第几个", function () {
|
||||
listenForChoice()
|
||||
})
|
||||
}
|
||||
|
||||
function listenForChoice() {
|
||||
state.awaitingChoice = true
|
||||
setTimeout(function () {
|
||||
start(true)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function chooseMenu(matches) {
|
||||
if (!matches.length) {
|
||||
notify("未找到可访问的菜单,请换个名称再试", "warning")
|
||||
return
|
||||
}
|
||||
if (matches.length === 1 || matches[0].score > matches[1].score) {
|
||||
openMenu(matches[0].menu)
|
||||
return
|
||||
}
|
||||
|
||||
state.pendingMatches = matches
|
||||
state.awaitingChoice = true
|
||||
showCandidateMessage(matches)
|
||||
|
||||
const prompt = matches
|
||||
.map(function (item, index) {
|
||||
return "第" + (index + 1) + "个," + item.menu.name
|
||||
})
|
||||
.join("。")
|
||||
speak("找到多个菜单,您需要打开第几个。" + prompt, function () {
|
||||
listenForChoice()
|
||||
})
|
||||
}
|
||||
|
||||
function updateButton(listening) {
|
||||
const $button = $("#voice-menu-btn")
|
||||
$button.toggleClass("is-listening", listening)
|
||||
$button.attr("title", listening ? "正在听,请说出菜单名称" : "语音打开菜单")
|
||||
$button.find(".voice-menu-text").text(listening ? "聆听中" : "语音")
|
||||
}
|
||||
|
||||
function start(choiceMode) {
|
||||
choiceMode = choiceMode || state.awaitingChoice
|
||||
if (!SpeechRecognition) {
|
||||
notify("当前浏览器不支持语音识别,请使用 Chrome 或 Edge", "warning")
|
||||
return
|
||||
}
|
||||
if (state.listening) {
|
||||
state.recognition.stop()
|
||||
return
|
||||
}
|
||||
|
||||
const recognition = new SpeechRecognition()
|
||||
recognition.lang = "zh-CN"
|
||||
recognition.interimResults = false
|
||||
recognition.continuous = false
|
||||
recognition.maxAlternatives = 1
|
||||
|
||||
recognition.onstart = function () {
|
||||
state.listening = true
|
||||
updateButton(true)
|
||||
notify(choiceMode ? "请说打开第几个" : "请说出要打开的菜单名称", "info")
|
||||
}
|
||||
recognition.onend = function () {
|
||||
state.listening = false
|
||||
updateButton(false)
|
||||
if (typeof state.afterRecognitionEnd === "function") {
|
||||
const next = state.afterRecognitionEnd
|
||||
state.afterRecognitionEnd = null
|
||||
setTimeout(next, 150)
|
||||
}
|
||||
}
|
||||
recognition.onerror = function (event) {
|
||||
const message = event.error === "not-allowed" ? "麦克风授权失败,请确认 HTTPS 或 localhost 环境并允许浏览器使用麦克风" : "语音识别失败,请再试一次"
|
||||
notify(message, "warning")
|
||||
}
|
||||
recognition.onresult = function (event) {
|
||||
const text = event.results && event.results[0] && event.results[0][0] && event.results[0][0].transcript
|
||||
state.lastText = text || ""
|
||||
if (!state.lastText) {
|
||||
notify("没有识别到语音内容", "warning")
|
||||
return
|
||||
}
|
||||
if (choiceMode || state.awaitingChoice) {
|
||||
runAfterRecognitionEnd(function () {
|
||||
handleChoice(state.lastText)
|
||||
})
|
||||
return
|
||||
}
|
||||
getMenus().then(function (menus) {
|
||||
const matches = matchMenus(state.lastText, menus)
|
||||
runAfterRecognitionEnd(function () {
|
||||
chooseMenu(matches)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
state.recognition = recognition
|
||||
recognition.start()
|
||||
}
|
||||
|
||||
function init() {
|
||||
$(document).on("click", "#voice-menu-btn", function () {
|
||||
start()
|
||||
})
|
||||
}
|
||||
|
||||
window.voiceMenuNavigator = {
|
||||
start,
|
||||
refreshMenus: function () {
|
||||
state.menus = null
|
||||
state.parentMap = {}
|
||||
}
|
||||
}
|
||||
|
||||
$(init)
|
||||
})(window, jQuery)
|
||||
Reference in New Issue
Block a user