first commit

This commit is contained in:
2026-08-26 14:25:17 +08:00
commit 06debfd1fc
10595 changed files with 1836924 additions and 0 deletions
@@ -0,0 +1,106 @@
if (typeof Vue === 'undefined') {
console.error('Vue is not defined. Please include Vue first.')
}
const MARK = '__autoEnhance_validated__'
function autoEnhanceForm(formVm) {
// 防止重复处理
if (formVm[MARK]) return
formVm[MARK] = true
const attrs = formVm.$attrs;
if (attrs && attrs['no-auto-enhance'] !== undefined) {
return; // 有 no-auto-enhance 属性,跳过增强
}
// 如果 $attrs 没有,也可以尝试从 $vnode 回退(兼容旧版)
if (!attrs) {
const vnode = formVm.$vnode;
if (vnode && vnode.data && vnode.data.attrs && vnode.data.attrs['no-auto-enhance'] !== undefined) {
return;
}
}
const originalValidate = formVm.validate
formVm.validate = function(callback) {
let capturedFields;
const promise = new Promise((resolve) => {
originalValidate.call(this, (valid, fields) => {
capturedFields = fields;
if (valid) {
resolve(true);
} else {
const firstField = Object.keys(fields)[0];
const fieldErrors = fields[firstField];
const errorMessage = fieldErrors?.[0]?.message || '校验失败';
let finalMessage = errorMessage;
const formItem = this.fields?.find((item) => item.prop === firstField);
if (errorMessage === '必填' && formItem && formItem.label) {
finalMessage = `${formItem.label} 必填`;
}
if (this.$message) {
this.$message({ message: finalMessage, type: 'error' });
} else {
alert(finalMessage);
}
this.$nextTick(() => {
const inputEl =
formItem?.$el?.querySelector('input, textarea, .el-input__inner, .el-textarea__inner');
if (inputEl) inputEl.focus();
});
resolve(false);
}
});
});
if (typeof callback === 'function') {
promise.then(valid => callback(valid, capturedFields));
return;
}
return promise;
}
}
// 拦截 Vue.component
if (Vue.component) {
const originalComponent = Vue.component
Vue.component = function(name, def) {
const res = originalComponent.apply(this, arguments)
if (name === 'el-form' && def) {
const originalMounted = def.mounted
def.mounted = function() {
autoEnhanceForm(this)
originalMounted?.call(this)
}
}
return res
}
}
if (Vue.extend) {
const originalExtend = Vue.extend
Vue.extend = function() {
const Ctor = originalExtend.apply(this, arguments)
if (Ctor.options?.name === 'ElForm') {
const originalMounted = Ctor.options.mounted
Ctor.options.mounted = function() {
autoEnhanceForm(this)
originalMounted?.call(this)
}
}
return Ctor
}
}
@@ -0,0 +1,350 @@
const commonUtil = {
//axios配置
axiosService() {
// 创建 axios 实例
const axiosService = axios.create({
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
"x-requested-with": "XMLHttpRequest"
}
})
//axios拦截器
axiosService.interceptors.response.use(
(response) => {
const userAgent = navigator.userAgent || navigator.vendor || window.opera
const isMobile = /iPad|iPhone|iPod|Android/.test(userAgent)
//判断是否为blob 不处理直接返回文件流
if (response.config.responseType === "blob") {
return response
}
if (response.data && response.data.code !== "200" && response.data.code !== 0 && response.data.code !== 99) {
if (isMobile) {
vant.Toast.fail(response.data.msg)
} else {
ELEMENT.Message.error(response.data.msg)
}
return Promise.reject(response.data)
}
return response.data
},
(error) => {
const userAgent = navigator.userAgent || navigator.vendor || window.opera
const isMobile = /iPad|iPhone|iPod|Android/.test(userAgent)
if (isMobile) {
vant.Toast(error.message)
} else {
ELEMENT.Message.error(error.message)
}
return Promise.reject(error)
}
)
return axiosService
},
//下载文件
downLoadService: function (url, data) {
const userAgent = navigator.userAgent || navigator.vendor || window.opera
const isMobile = /iPad|iPhone|iPod|Android/.test(userAgent)
let loading = null
if (isMobile) {
loading = vant.Toast.loading({
duration: 0,
forbidClick: true,
message: '导出中,请耐心等待'
});
} else {
loading = ELEMENT.Loading.service({
lock: true,
text: "导出中,请耐心等待",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)"
})
}
Vue.prototype.$axios
.post(url, data, {responseType: "blob"})
.then((response) => {
if (response.data.type === "application/json") {
try {
const reader = new FileReader()
reader.onload = function () {
// reader.result 包含了 Blob 的内容,转换为字符串
const content = reader.result
// 将字符串解析为 JSON 对象
const jsonObject = JSON.parse(content)
ELEMENT.Message.error(jsonObject.msg)
}
reader.readAsText(response.data)
} catch (err) {
ELEMENT.Message.error("下载文件出错")
}
return
}
//获取服务器返回的文件描述信息
const contentDisposition = response.headers["content-disposition"]
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
const matches = filenameRegex.exec(contentDisposition)
let filename = ""
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, "")
filename = decodeURIComponent(filename)
}
//执行下载文件
const url = window.URL.createObjectURL(new Blob([response.data]))
const link = document.createElement("a")
link.href = url
link.setAttribute("download", filename) // 例如 'document.pdf'
document.body.appendChild(link)
link.click()
link.parentNode.removeChild(link)
loading.close()
})
.finally(() => {
if (isMobile) {
loading.clear()
} else {
loading.close()
}
})
},
//权限认证
authService() {
function authPermission(permission) {
if (typeof permission !== "string" || !permission.trim()) {
return false
}
const permissions = store.state.user.permissions
if (!Array.isArray(permissions) || permissions.length === 0) {
return false
}
const permissionsSet = new Set(permissions)
return permissionsSet.has(permission.trim())
}
function authRole(role) {
if (typeof role !== "string" || !role.trim()) {
return false
}
const roles = store.state.user.roles.map((item) => item.code)
if (!Array.isArray(roles) || roles.length === 0) {
return false
}
const rolesSet = new Set(roles)
return rolesSet.has(role.trim())
}
return {
// 验证用户是否具备某权限
hasPermission(permission) {
return authPermission(permission)
},
// 验证用户是否含有指定权限,只需包含其中一个
hasPermissionOr(permissionsOrPermissionsStr) {
const permissions = Array.isArray(permissionsOrPermissionsStr) ? permissionsOrPermissionsStr : permissionsOrPermissionsStr.split(",")
return permissions.some((item) => {
return authPermission(item)
})
},
// 验证用户是否含有指定权限,必须全部拥有
hasPermissionAnd(permissionsOrPermissionsStr) {
const permissions = Array.isArray(permissionsOrPermissionsStr) ? permissionsOrPermissionsStr : permissionsOrPermissionsStr.split(",")
return permissions.every((item) => {
return authPermission(item)
})
},
// 判断用户是否拥有某个角色
hasRole(role) {
return authRole(role)
},
// 判断用户是否拥有某些角色中的任意一个
hasRoleOr(rolesOrRoleStr) {
const roles = Array.isArray(rolesOrRoleStr) ? rolesOrRoleStr : rolesOrRoleStr.split(",")
return roles.some((item) => {
return authRole(item.trim())
})
},
// 判断用户是否拥有某些角色中的所有
hasRoleAnd(rolesOrRoleStr) {
const roles = Array.isArray(rolesOrRoleStr) ? rolesOrRoleStr : rolesOrRoleStr.split(",")
return roles.every((item) => {
return authRole(item)
})
}
}
},
//根据文件后缀名获取显示的svg路径
getFileItemShowIcon(suffix) {
let assets = "/assets/platform/img/svg/"
if (["doc", "docx"].includes(suffix)) {
assets += "docx.svg"
} else if (["xls", "xlsx"].includes(suffix)) {
assets += "xlsx.svg"
} else if (["ppt", "pptx"].includes(suffix)) {
assets += "pptx.svg"
} else if (["pdf"].includes(suffix)) {
assets += "pdf.svg"
} else if (["txt"].includes(suffix)) {
assets += "txt.svg"
} else if (["zip", "rar"].includes(suffix)) {
assets += "zip.svg"
} else if (["mp4", "avi", "wmv", "rmvb", "flv", "mkv"].includes(suffix)) {
assets += "mp4.svg"
} else if (["mp3", "wav", "wma", "flac", "ape"].includes(suffix)) {
assets += "mp3.svg"
} else if (["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(suffix)) {
assets += "image.svg"
} else {
assets += "unknow.svg"
}
return assets
},
//文件预览
previewFile(item) {
if (["doc", "docx", "xls", "xlsx"].includes(item.suffix.toLocaleLowerCase())) {
window.open(
"/assets/platform/plugins/pdfJs/web/viewer.html?file=" + encodeURIComponent("/platform/sys/file/convertPDF?id=" + item.id),
item.name
)
} else if (["pdf"].includes(item.suffix.toLocaleLowerCase())) {
window.open("/assets/platform/plugins/pdfJs/web/viewer.html?file=" + encodeURIComponent(item.downloadPath), item.name)
} else if (["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(item.suffix.toLocaleLowerCase())) {
let image = new Image()
image.src = item.downloadPath
let viewer = new Viewer(image, {
zIndex: 99999,
title: false
})
viewer.show()
} else {
// this.$message.warning("暂不支持预览该文件,请下载后查看")
alert("暂不支持预览该文件,请下载后查看")
}
},
smCrypto() {
const cipherMode = 1 // 1 - C1C3C20 - C1C2C3,默认为1
const publicKey =
"04298364ec840088475eae92a591e01284d1abefcda348b47eb324bb521bb03b0b2a5bc393f6b71dabb8f15c99a0050818b56b23f31743b93df9cf8948f15ddb54"
// SM2加密
function doSm2Encrypt(msgString) {
return sm2.doEncrypt(msgString, publicKey, cipherMode)
}
// SM2数组加密
function doSm2ArrayEncrypt(msgString) {
return sm2.doEncrypt(msgString, publicKey, cipherMode)
}
return {
doSm2Encrypt,
doSm2ArrayEncrypt
}
},
// pjax跳转
pjaxPush(url, data = {}) {
const {pathname, search} = location
if (url === pathname + search) {
return
}
$.pjax({
url: url,
container: "#sub-app-container-main-content-body",
maxCacheLength: 0,
push: false,
replace: true,
fragment: "#sub-app-container-main-content-body",
timeout: 8000,
data
})
}
}
function removeHTMLTag(str) {
str = str.replace(/<\/?[^>]*>/g, "") //去除HTML tag
str = str.replace(/[ | ]*\n/g, "\n") //去除行尾空白
str = str.replace(/\n[\s| | ]*\r/g, "\n") //去除多余空行
str = str.replace(/&nbsp;/gi, "") //去掉&nbsp;
return str
}
function GetQueryString(name) {
return new URLSearchParams(window.location.search).get(name)
}
function clearAllTimers() {
const maxTimeoutId = setTimeout(function () {
}, 0)
for (let i = 0; i <= maxTimeoutId; i++) {
clearTimeout(i)
clearInterval(i)
}
}
function getNestedValue(obj, path) {
return path.split(".").reduce((acc, part) => acc && acc[part], obj)
}
function loc() {
return location.href.replace(location.search, "").replace(location.hash, "")
}
function clone(obj) {
return JSON.parse(JSON.stringify(obj))
}
function base64ToFile(base64Data, filename) {
// 将base64的数据部分提取出来
const parts = base64Data.split(";base64,")
const contentType = parts[0].split(":")[1]
const raw = window.atob(parts[1])
const rawLength = raw.length
const uInt8Array = new Uint8Array(rawLength)
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i)
}
// 使用Blob对象创建File对象
const blob = new Blob([uInt8Array], {type: contentType})
blob.lastModifiedDate = new Date()
blob.name = filename
return new File([blob], filename, {type: contentType})
}
function createLoading(text = '加载中...') {
return ELEMENT.Loading.service({
lock: true,
text: text,
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
})
}
function getBaseSubAppPath(pathname = window.location.pathname) {
const startIndex = pathname.indexOf('/platform');
if (startIndex === -1) {
return pathname
}
return pathname.substring(startIndex);
}
function getFullSubAppPath() {
const pathname = window.location.pathname;
const search = window.location.search; // 包含 ?appId=...
const startIndex = pathname.indexOf('/platform');
if (startIndex === -1) {
return pathname
}
return pathname.substring(startIndex) + search;
}
@@ -0,0 +1,91 @@
// =============== 坐标系工具 ===============
const PI = 3.1415926535897932384626
const a = 6378245.0 //卫星椭球坐标投影到平面地图坐标系的投影因子。
const ee = 0.00669342162296594323 //椭球的偏心率。
const coordinateUtil = {
// 判断是否在中国范围(仅中国加密)
outOfChina(lon, lat) {
if (lon < 72.004 || lon > 137.8347) {
return true;
}
if (lat < 0.8293 || lat > 55.8271) {
return true;
}
return false;
},
// 基础偏移计算
transformLat(lng, lat) {
let ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng))
ret += ((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0) / 3.0
ret += ((20.0 * Math.sin(lat * PI) + 40.0 * Math.sin((lat / 3.0) * PI)) * 2.0) / 3.0
ret += ((160.0 * Math.sin((lat / 12.0) * PI) + 320 * Math.sin((lat * PI) / 30.0)) * 2.0) / 3.0
return ret
},
transformLng(lng, lat) {
let ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng))
ret += ((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0) / 3.0
ret += ((20.0 * Math.sin(lng * PI) + 40.0 * Math.sin((lng / 3.0) * PI)) * 2.0) / 3.0
ret += ((150.0 * Math.sin((lng / 12.0) * PI) + 300.0 * Math.sin((lng / 30.0) * PI)) * 2.0) / 3.0
return ret
},
// WGS-84 → GCJ-02(用于地图展示)
wgs84ToGcj02(lng, lat) {
let dlat = this.transformLat(lng - 105.0, lat - 35.0);
let dlng = this.transformLng(lng - 105.0, lat - 35.0);
let radlat = (lat / 180.0) * PI;
let magic = Math.sin(radlat);
magic = 1 - ee * magic * magic;
let sqrtmagic = Math.sqrt(magic);
dlat =
(dlat * 180.0) /
(((a * (1 - ee)) / (magic * sqrtmagic)) * PI);
dlng =
(dlng * 180.0) / ((a / sqrtmagic) * Math.cos(radlat) * PI);
let mglat = lat + dlat;
let mglng = lng + dlng;
return [mglat, mglng];
},
// GCJ-02 → WGS-84(用于存储签到点)
gcj02ToWgs84(lng, lat) {
const originalLngSign = Math.sign(lng);
const originalLatSign = Math.sign(lat);
lat = Math.abs(lat);
lng = Math.abs(lng);
let dlat = this.transformLat(lng - 105.0, lat - 35.0)
let dlng = this.transformLng(lng - 105.0, lat - 35.0)
let radlat = lat / 180.0 * PI
let magic = Math.sin(radlat)
magic = 1 - ee * magic * magic
let sqrtmagic = Math.sqrt(magic)
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI)
dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI)
let mglat = lat + dlat
let mglng = lng + dlng
let lngs = lng * 2 - mglng
let lats = lat * 2 - mglat
let finalLng = originalLngSign * lngs;
let finalLat = originalLatSign * lats;
return [finalLat, finalLng];
},
// 计算两点间距离(米),输入 WGS-84 坐标
getDistance(lat1, lng1, lat2, lng2) {
const R = 6371000; // 地球半径(米)
const φ1 = lat1 * Math.PI / 180;
const φ2 = lat2 * Math.PI / 180;
const Δφ = (lat2 - lat1) * Math.PI / 180;
const Δλ = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
},
};
@@ -0,0 +1,56 @@
// 劫持 validate:失败时先提示 + 滚动,再抛出异常(保持原有行为)
if (typeof Vue !== 'undefined') {
Vue.mixin({
mounted() {
this.$nextTick(() => {
const formRefs = Object.keys(this.$refs).filter(ref => {
const comp = this.$refs[ref];
return comp &&
comp.$options &&
comp.$options._componentTag === 'van-form';
});
formRefs.forEach(refName => {
const form = this.$refs[refName];
const attrs = form.$attrs;
if (attrs && attrs['no-auto-enhance'] !== undefined) {
return; // 有 no-auto-enhance 属性,跳过增强
}
if (!form || form._validatedEnhanced) return;
const originalValidate = form.validate;
form.validate = function () {
// 保留原 validate 的返回值(Promise
const promise = originalValidate.call(this);
return promise.catch(errorInfo => {
if (errorInfo?.length > 0) {
const firstError = errorInfo[0];
if(firstError.message) {
Vue.prototype.$toast?.fail(firstError.message);
} else {
const fieldName = firstError.name;
// 滚动到字段
// this.scrollToField(fieldName);
// 获取 label
const field = this.fields?.find(f => f.name === fieldName);
const label = field?.label;
// 提示用户
Vue.prototype.$toast?.fail(`${label} 必填`);
}
}
return new Promise(() => {});
});
};
form._validatedEnhanced = true;
});
});
}
});
}
@@ -0,0 +1,460 @@
;(function (window, $) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
const commandWords = [
"打开",
"进入",
"跳转",
"跳到",
"去",
"访问",
"帮我",
"请",
"页面",
"菜单",
"一下"
]
const state = {
recognition: null,
listening: false,
menus: null,
platform: "PC",
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;")
}
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 getCurrentPlatform() {
const $button = $("#voice-menu-btn")
return String(($button.data("platform") || state.platform || "PC")).toUpperCase()
}
function setCurrentPlatform(platform) {
const nextPlatform = String(platform || "PC").toUpperCase()
if (state.platform !== nextPlatform) {
state.menus = null
state.parentMap = {}
}
state.platform = nextPlatform
return state.platform
}
function filterMenusByPlatform(menus, platform) {
const currentPlatform = String(platform || "PC").toUpperCase()
return (menus || []).filter(function (menu) {
return String(menu.platform || "PC").toUpperCase() === currentPlatform
})
}
function setMenus(menus, platform) {
state.parentMap = {}
state.menus = filterMenusByPlatform(flattenMenus(menus, "", []), platform).filter(function (menu) {
return menu.href
})
return state.menus
}
function getMenus(platform) {
platform = String(platform || getCurrentPlatform()).toUpperCase()
setCurrentPlatform(platform)
const cached = state.menus
if (cached && cached.length) {
return $.Deferred().resolve(cached).promise()
}
const storeMenus = getStoreMenus()
if (storeMenus && storeMenus.length) {
return $.Deferred().resolve(setMenus(storeMenus, platform)).promise()
}
return $.get("/platform/sys/user/getLogonUser").then(function (res) {
if (res && res.code === 0 && res.data && res.data.menus) {
return setMenus(res.data.menus, platform)
}
const sessionMenus = getSessionMenus()
if (sessionMenus && sessionMenus.length) {
return setMenus(sessionMenus, platform)
}
return []
}, function () {
const sessionMenus = getSessionMenus()
if (sessionMenus && sessionMenus.length) {
return setMenus(sessionMenus, platform)
}
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(getCurrentPlatform()).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 () {
setCurrentPlatform($(this).data("platform") || "PC")
start()
})
}
window.voiceMenuNavigator = {
start,
refreshMenus: function () {
state.menus = null
state.parentMap = {}
}
}
$(init)
})(window, jQuery)