first commit
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
;(function (a) {
|
||||
;(jQuery.browser = jQuery.browser || {}).mobile =
|
||||
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(
|
||||
a
|
||||
) ||
|
||||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
|
||||
a.substr(0, 4)
|
||||
)
|
||||
})(navigator.userAgent || navigator.vendor || window.opera)
|
||||
|
||||
$.ajaxSetup({
|
||||
contentType: "application/x-www-form-urlencoded;charset=utf-8",
|
||||
success: function (response, textStatus, jqXHR) {
|
||||
if (response.code !== 0) {
|
||||
this.$message.error(response.msg)
|
||||
} else {
|
||||
return response
|
||||
}
|
||||
},
|
||||
error: function (v1) {
|
||||
this.$message.error(v1.status + " " + v1.statusText)
|
||||
},
|
||||
complete: function (XMLHttpRequest, textStatus) {
|
||||
var sessionstatus = XMLHttpRequest.getResponseHeader("loginStatus") //通过XMLHttpRequest取得响应头,sessionstatus,
|
||||
if (sessionstatus == "accessDenied") {
|
||||
this.$message({
|
||||
message: "\u767b\u5f55\u5931\u6548\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u91cd\u65b0\u767b\u5f55",
|
||||
type: "error"
|
||||
})
|
||||
}
|
||||
if (sessionstatus == "unauthorized") {
|
||||
this.$message({
|
||||
message: "\u6ca1\u6709\u6743\u9650",
|
||||
type: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function pjaxReplace(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
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
const flowMixins = {
|
||||
created() {
|
||||
this.$nextTick(async ()=>{
|
||||
await httpVueLoader("/components/plugins/Guava.vue?v=" + new Date().getTime())()
|
||||
this.checkFlowTask();
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
checkFlowTask() {
|
||||
const taskId = GetQueryString("taskId")
|
||||
if (!taskId) return
|
||||
|
||||
// 如果是开始任务,用户自己处理(编辑、删除、撤销等等)
|
||||
const taskKey = GetQueryString("taskKey")
|
||||
if (taskKey === 'startTask') return;
|
||||
|
||||
// 增加loading动画
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: 'Loading',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
|
||||
let delayCloseLoading = false
|
||||
this.$axios.post(location.pathname + "/queryTask", {taskId}).then((res) => {
|
||||
if (res.code === 0 && res.data) {
|
||||
delayCloseLoading = true
|
||||
// Guava 为异步组件,等待组件实例完成挂载后再打开任务弹窗。
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (res.data.taskState === 10) {
|
||||
this.openAudit(res.data)
|
||||
} else {
|
||||
this.openView(res.data)
|
||||
}
|
||||
} finally {
|
||||
loading.close()
|
||||
}
|
||||
}, 1500)
|
||||
}
|
||||
}).finally(() => {
|
||||
// 查询失败或未返回任务数据时,必须关闭全屏 Loading。
|
||||
if (!delayCloseLoading) {
|
||||
loading.close()
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
const initTableMixins = {
|
||||
data() {
|
||||
return {
|
||||
pageDataUrl: "",
|
||||
submitLoading: false,
|
||||
searchMore: false,
|
||||
tableSize: "",
|
||||
tableKey: "",
|
||||
formData: {},
|
||||
formRules: {},
|
||||
formLoading: false,
|
||||
tableLoading: false,
|
||||
tableData: [],
|
||||
tableColumns: [],
|
||||
pageForm: {
|
||||
searchName: "",
|
||||
searchKeyword: "",
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
pageOrderName: "",
|
||||
pageOrderBy: "",
|
||||
audit: false
|
||||
},
|
||||
guavaIndex: "index",
|
||||
unionGroups: [],
|
||||
threeUnits: [],
|
||||
|
||||
dialogFormVisible: false,
|
||||
|
||||
viewData: {},
|
||||
viewDialogVisible: false,
|
||||
|
||||
_approvalInitialized: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
dropdownCommand({ action, value }) {
|
||||
if (action) action(value)
|
||||
},
|
||||
columnChange(val) {
|
||||
this.tableLoading = true
|
||||
this.tableColumns = val
|
||||
this.tableLoading = false
|
||||
},
|
||||
tableSizeChange(size) {
|
||||
this.tableSize = size
|
||||
},
|
||||
indexMethod(index) {
|
||||
return index + (this.pageForm.pageNumber - 1) * this.pageForm.pageSize + 1
|
||||
},
|
||||
doSearch() {
|
||||
this.tableKey = new Date().getTime()
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageData()
|
||||
},
|
||||
pageOrder(column) {
|
||||
this.pageForm.pageOrderName = column.prop
|
||||
this.pageForm.pageOrderBy = column.order
|
||||
this.pageData()
|
||||
},
|
||||
pageNumberChange(val) {
|
||||
this.pageForm.pageNumber = val
|
||||
this.pageData()
|
||||
},
|
||||
pageSizeChange(val) {
|
||||
this.pageForm.pageSize = val
|
||||
this.pageData()
|
||||
},
|
||||
pageData(data = null) {
|
||||
const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData"
|
||||
this.tableLoading = true
|
||||
this.$axios.post(address, data ? data : this.pageForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.tableData = res.data.list
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
}
|
||||
}).finally(() => {
|
||||
// 接口异常时同样结束加载状态,避免表格持续显示加载动画。
|
||||
this.tableLoading = false
|
||||
})
|
||||
},
|
||||
notifySuccess(msg) {
|
||||
this.$notify({
|
||||
title: "成功",
|
||||
message: msg,
|
||||
type: "success"
|
||||
})
|
||||
},
|
||||
notifyWarning(msg) {
|
||||
this.$notify({
|
||||
title: "警告",
|
||||
message: msg,
|
||||
type: "warning"
|
||||
})
|
||||
},
|
||||
notifyError(msg) {
|
||||
this.$notify.error({
|
||||
title: "错误",
|
||||
message: msg
|
||||
})
|
||||
},
|
||||
initApprovalFromUrl() {
|
||||
if (this._approvalInitialized) return;
|
||||
|
||||
if (!this.pageForm) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 URL 读取 approval 参数(适用于独立页面)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const approvalParam = urlParams.get('tab');
|
||||
|
||||
if (approvalParam !== null) {
|
||||
this.$set(this.pageForm, 'approval', approvalParam === 'done')
|
||||
this.$set(this.pageForm, 'isAudit', approvalParam === 'done')
|
||||
this._approvalInitialized = true;
|
||||
}
|
||||
},
|
||||
fileHandleRemove(file, fileList) {
|
||||
return fileList;
|
||||
},
|
||||
fileHandleChange(file, fileList, {type, size}) {
|
||||
const removeFile = () => {
|
||||
fileList.splice(fileList.findIndex(v => v === file))
|
||||
}
|
||||
|
||||
if (!file.size) {
|
||||
this.notifyWarning('您选择的是空文件!')
|
||||
removeFile()
|
||||
}
|
||||
|
||||
if (type && type.length && !type.includes(file.name.split('.').pop().toLowerCase())) {
|
||||
this.notifyWarning(`文件只能是 ${type.map(v => v.toUpperCase()).join('/')} 格式!`)
|
||||
removeFile()
|
||||
}
|
||||
|
||||
if (size && !file.size < size) {
|
||||
this.notifyWarning(`文件大小不能超过 ${size / 1024 / 1024}MB!`)
|
||||
removeFile()
|
||||
}
|
||||
|
||||
return fileList;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initApprovalFromUrl();
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
const styleUtil = {
|
||||
// 为元素及其所有子元素添加scoped属性
|
||||
addScopedAttribute(el, scopedId, vm) {
|
||||
if (!el || !el.setAttribute) return;
|
||||
|
||||
// 跳过Vue组件实例 但是不跳过自己 也就是排除子组件
|
||||
if (el.__vue__ && el.__vue__ !== vm) return;
|
||||
|
||||
// 添加scoped属性到当前元素
|
||||
el.setAttribute(scopedId, '')
|
||||
|
||||
// 只处理普通DOM元素,不处理Vue组件
|
||||
// const children = el.children || el.childNodes;
|
||||
// for (let i = 0; i < children.length; i++) {
|
||||
// const child = children[i];
|
||||
// // 检查是否为普通DOM元素(非Vue组件)
|
||||
// if (child.nodeType === 1 && !child.__vue__) {
|
||||
// styleUtil.addScopedAttribute(child, scopedId, vm);
|
||||
// }
|
||||
// }
|
||||
|
||||
// 查找本组件的子元素 但是排除掉VUE组件 使用TreeWalker优化遍历性能
|
||||
const walker = document.createTreeWalker(
|
||||
el,
|
||||
NodeFilter.SHOW_ELEMENT,
|
||||
{
|
||||
acceptNode(node) {
|
||||
// 排除子Vue组件
|
||||
return node.__vue__ && node !== el
|
||||
? NodeFilter.FILTER_REJECT
|
||||
: NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
if (node !== el) {
|
||||
node.setAttribute(scopedId, '');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 移除元素及其所有子元素的scoped属性
|
||||
removeScopedAttribute(el, scopedId) {
|
||||
if (!el || !el.removeAttribute) return;
|
||||
|
||||
// 使用querySelectorAll批量处理
|
||||
const elements = el.querySelectorAll(`[${scopedId}]`);
|
||||
elements.forEach(element => element.removeAttribute(scopedId));
|
||||
|
||||
// 处理根元素
|
||||
el.removeAttribute(scopedId);
|
||||
},
|
||||
|
||||
// 为CSS规则添加作用域属性
|
||||
scopeCss(css, scopedId) {
|
||||
// 使用正则表达式匹配CSS选择器和规则块
|
||||
// 这种方法可以确保我们只处理选择器部分,而不会影响CSS属性值
|
||||
return css.replace(/([^{]+)({[^}]*})/g, (match, selectors, rules) => {
|
||||
// 处理选择器部分
|
||||
const processedSelectors = selectors.split(',')
|
||||
.map(selector => {
|
||||
selector = selector.trim();
|
||||
|
||||
// 处理深度选择器
|
||||
if (selector.includes('::v-deep')) {
|
||||
return selector.replace('::v-deep', `[${scopedId}]`);
|
||||
}
|
||||
if(selector.includes("/deep/")){
|
||||
return selector.replace('/deep/', `[${scopedId}]`);
|
||||
}
|
||||
|
||||
|
||||
// 处理媒体查询等特殊情况
|
||||
if (selector.includes('@') || selector.startsWith('@')) {
|
||||
return selector;
|
||||
}
|
||||
|
||||
// 普通选择器
|
||||
return `${selector}[${scopedId}]`;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
// 返回处理后的选择器和原始规则块
|
||||
return processedSelectors + rules;
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
// 异步DOM观察器
|
||||
const asyncStyleHandler = {
|
||||
observers: new Map(),
|
||||
|
||||
// 为组件设置观察器
|
||||
setupObserver(vm, scopedId) {
|
||||
if (!vm.$el) return;
|
||||
|
||||
// 清除现有观察器
|
||||
this.disconnectObserver(vm._uid);
|
||||
|
||||
// 创建新的MutationObserver
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
let shouldProcess = false;
|
||||
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
||||
shouldProcess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldProcess) {
|
||||
// 防抖处理
|
||||
clearTimeout(vm._styleDebounce);
|
||||
vm._styleDebounce = setTimeout(() => {
|
||||
styleUtil.addScopedAttribute(vm.$el, scopedId, vm);
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
// 开始观察
|
||||
observer.observe(vm.$el, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// 存储观察器引用
|
||||
this.observers.set(vm._uid, observer);
|
||||
},
|
||||
|
||||
// 断开观察器
|
||||
disconnectObserver(uid) {
|
||||
if (this.observers.has(uid)) {
|
||||
this.observers.get(uid).disconnect();
|
||||
this.observers.delete(uid);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Vue.mixin({
|
||||
mounted() {
|
||||
if (!this.$options.style) return;
|
||||
|
||||
try {
|
||||
const styleId = `style-${this._uid}`;
|
||||
const scopedId = `data-v-${this._uid}`;
|
||||
|
||||
// 检查样式是否已存在
|
||||
if (document.getElementById(styleId)) {
|
||||
console.warn(`Style with id ${styleId} already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = styleId;
|
||||
|
||||
// 添加作用域属性
|
||||
if (this.$el) {
|
||||
styleUtil.addScopedAttribute(this.$el, scopedId, this);
|
||||
}
|
||||
|
||||
// 处理CSS
|
||||
let css = typeof this.$options.style === "function"
|
||||
? this.$options.style.call(this)
|
||||
: this.$options.style;
|
||||
|
||||
if (!css || typeof css !== 'string') {
|
||||
console.warn('Invalid style content');
|
||||
return;
|
||||
}
|
||||
|
||||
css = styleUtil.scopeCss(css, scopedId);
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
this._scopedId = scopedId;
|
||||
this._styleId = styleId;
|
||||
|
||||
// 设置异步DOM观察器
|
||||
asyncStyleHandler.setupObserver(this, scopedId);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error applying scoped styles:', error);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
beforeDestroy() {
|
||||
try {
|
||||
// 清理作用域属性
|
||||
if (this._scopedId && this.$el) {
|
||||
styleUtil.removeScopedAttribute(this.$el, this._scopedId);
|
||||
}
|
||||
|
||||
// 移除样式标签
|
||||
if (this._styleId) {
|
||||
const style = document.getElementById(this._styleId);
|
||||
if (style && style.parentNode) {
|
||||
style.parentNode.removeChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
// 断开观察器
|
||||
asyncStyleHandler.disconnectObserver(this._uid);
|
||||
} catch (error) {
|
||||
console.error('Error cleaning up scoped styles:', error);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,121 @@
|
||||
//业务工具
|
||||
const businessTool = {
|
||||
listUnion: (unionId = null) => {
|
||||
return commonUtil
|
||||
.axiosService()
|
||||
.post("/open/common/unionOptions", {unionId})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
return resp.data
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
},
|
||||
listClub: (clubId = null) => {
|
||||
return commonUtil
|
||||
.axiosService()
|
||||
.post("/open/common/clubOptions", {clubId})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
return resp.data
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
},
|
||||
listUnit(unionId = null) {
|
||||
return commonUtil
|
||||
.axiosService()
|
||||
.post("/open/common/unitOptions", {unionId})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
return resp.data
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
},
|
||||
listClubByRole() {
|
||||
return commonUtil
|
||||
.axiosService()
|
||||
.post("/platform/club/common/listClubByRole", {})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
return resp.data
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
},
|
||||
listManageClubByRole() {
|
||||
return commonUtil
|
||||
.axiosService()
|
||||
.post("/platform/club/common/listManageClubByRole", {})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
return resp.data
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 获取字典数据,不包含禁用
|
||||
* @param code
|
||||
* @returns {*}
|
||||
*/
|
||||
getDictOptions(code = null) {
|
||||
return commonUtil
|
||||
.axiosService()
|
||||
.post("/open/common/dictOptions", {code})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
return resp.data
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 包含禁用
|
||||
* @param code
|
||||
* @returns {*}
|
||||
*/
|
||||
getAllDictOptions(code = null) {
|
||||
return commonUtil
|
||||
.axiosService()
|
||||
.post("/open/common/dictAllOptions", {code})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
return resp.data
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
},
|
||||
getConfigKey(key) {
|
||||
return commonUtil
|
||||
.axiosService()
|
||||
.post("/open/common/getConfigKey", {key})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
return resp.data
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
})
|
||||
},
|
||||
getEnumOptions(name = null) {
|
||||
return commonUtil
|
||||
.axiosService()
|
||||
.post("/open/common/dictEnumOptions", {name})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
return resp.data
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 - C1C3C2,0 - 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(/ /gi, "") //去掉
|
||||
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, "&")
|
||||
.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 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)
|
||||
Reference in New Issue
Block a user