20 changed files with 6098 additions and 493 deletions
-
2public/index.html
-
3514public/static/adapter.min.js
-
305public/static/webrtcstreamer.js
-
313public/static/webrtcstreamer2.js
-
10src/api/area/index.js
-
10src/assets/styles/manage.scss
-
26src/router/routers.js
-
32src/views/components/bookSwiper.vue
-
317src/views/components/canvasPreview.vue
-
61src/views/components/hkVideo.vue
-
14src/views/home.vue
-
88src/views/visualCheck/bookstore/collection/index.vue
-
293src/views/visualCheck/checkManage/dataScreening/girdList.vue
-
318src/views/visualCheck/checkManage/dataScreening/index.vue
-
331src/views/visualCheck/checkManage/dataScreening/regionsList.vue
-
672src/views/visualCheck/checkManage/dataScreening/shelfList.vue
-
151src/views/visualCheck/checkManage/index.vue
-
51src/views/visualCheck/venueDevice/area/index.vue
-
59src/views/visualCheck/venueDevice/bookshelfPosition/index.vue
-
2src/views/visualCheck/venueDevice/floor/index.vue
3514
public/static/adapter.min.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,305 @@ |
|||||
|
var WebRtcStreamer = (function() { |
||||
|
|
||||
|
/** |
||||
|
* Interface with WebRTC-streamer API |
||||
|
* @constructor |
||||
|
* @param {string} videoElement - id of the video element tag |
||||
|
* @param {string} srvurl - url of webrtc-streamer (default is current location) |
||||
|
*/ |
||||
|
var WebRtcStreamer = function WebRtcStreamer (videoElement, srvurl) { |
||||
|
if (typeof videoElement === "string") { |
||||
|
this.videoElement = document.getElementById(videoElement); |
||||
|
} else { |
||||
|
this.videoElement = videoElement; |
||||
|
} |
||||
|
this.srvurl = srvurl || location.protocol+"//"+window.location.hostname+":"+window.location.port; |
||||
|
this.pc = null; |
||||
|
|
||||
|
this.mediaConstraints = { offerToReceiveAudio: true, offerToReceiveVideo: true }; |
||||
|
|
||||
|
this.iceServers = null; |
||||
|
this.earlyCandidates = []; |
||||
|
} |
||||
|
|
||||
|
WebRtcStreamer.prototype._handleHttpErrors = function (response) { |
||||
|
if (!response.ok) { |
||||
|
throw Error(response.statusText); |
||||
|
} |
||||
|
return response; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Connect a WebRTC Stream to videoElement |
||||
|
* @param {string} videourl - id of WebRTC video stream |
||||
|
* @param {string} audiourl - id of WebRTC audio stream |
||||
|
* @param {string} options - options of WebRTC call |
||||
|
* @param {string} stream - local stream to send |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.connect = function(videourl, audiourl, options, localstream) { |
||||
|
this.disconnect(); |
||||
|
|
||||
|
// getIceServers is not already received
|
||||
|
if (!this.iceServers) { |
||||
|
console.log("Get IceServers"); |
||||
|
|
||||
|
fetch(this.srvurl + "/api/getIceServers") |
||||
|
.then(this._handleHttpErrors) |
||||
|
.then( (response) => (response.json()) ) |
||||
|
.then( (response) => this.onReceiveGetIceServers(response, videourl, audiourl, options, localstream)) |
||||
|
.catch( (error) => this.onError("getIceServers " + error )) |
||||
|
|
||||
|
} else { |
||||
|
this.onReceiveGetIceServers(this.iceServers, videourl, audiourl, options, localstream); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Disconnect a WebRTC Stream and clear videoElement source |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.disconnect = function() { |
||||
|
if (this.videoElement?.srcObject) { |
||||
|
this.videoElement.srcObject.getTracks().forEach(track => { |
||||
|
track.stop() |
||||
|
this.videoElement.srcObject.removeTrack(track); |
||||
|
}); |
||||
|
} |
||||
|
if (this.pc) { |
||||
|
fetch(this.srvurl + "/api/hangup?peerid=" + this.pc.peerid) |
||||
|
.then(this._handleHttpErrors) |
||||
|
.catch( (error) => this.onError("hangup " + error )) |
||||
|
|
||||
|
|
||||
|
try { |
||||
|
this.pc.close(); |
||||
|
} |
||||
|
catch (e) { |
||||
|
console.log ("Failure close peer connection:" + e); |
||||
|
} |
||||
|
this.pc = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
* GetIceServers callback |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.onReceiveGetIceServers = function(iceServers, videourl, audiourl, options, stream) { |
||||
|
this.iceServers = iceServers; |
||||
|
this.pcConfig = iceServers || {"iceServers": [] }; |
||||
|
try { |
||||
|
this.createPeerConnection(); |
||||
|
|
||||
|
var callurl = this.srvurl + "/api/call?peerid=" + this.pc.peerid + "&url=" + encodeURIComponent(videourl); |
||||
|
if (audiourl) { |
||||
|
callurl += "&audiourl="+encodeURIComponent(audiourl); |
||||
|
} |
||||
|
if (options) { |
||||
|
callurl += "&options="+encodeURIComponent(options); |
||||
|
} |
||||
|
|
||||
|
if (stream) { |
||||
|
this.pc.addStream(stream); |
||||
|
} |
||||
|
|
||||
|
// clear early candidates
|
||||
|
this.earlyCandidates.length = 0; |
||||
|
|
||||
|
// create Offer
|
||||
|
this.pc.createOffer(this.mediaConstraints).then((sessionDescription) => { |
||||
|
console.log("Create offer:" + JSON.stringify(sessionDescription)); |
||||
|
|
||||
|
this.pc.setLocalDescription(sessionDescription) |
||||
|
.then(() => { |
||||
|
fetch(callurl, { method: "POST", body: JSON.stringify(sessionDescription) }) |
||||
|
.then(this._handleHttpErrors) |
||||
|
.then( (response) => (response.json()) ) |
||||
|
.catch( (error) => this.onError("call " + error )) |
||||
|
.then( (response) => this.onReceiveCall(response) ) |
||||
|
.catch( (error) => this.onError("call " + error )) |
||||
|
|
||||
|
}, (error) => { |
||||
|
console.log ("setLocalDescription error:" + JSON.stringify(error)); |
||||
|
}); |
||||
|
|
||||
|
}, (error) => { |
||||
|
alert("Create offer error:" + JSON.stringify(error)); |
||||
|
}); |
||||
|
|
||||
|
} catch (e) { |
||||
|
this.disconnect(); |
||||
|
alert("connect error: " + e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
WebRtcStreamer.prototype.getIceCandidate = function() { |
||||
|
fetch(this.srvurl + "/api/getIceCandidate?peerid=" + this.pc.peerid) |
||||
|
.then(this._handleHttpErrors) |
||||
|
.then( (response) => (response.json()) ) |
||||
|
.then( (response) => this.onReceiveCandidate(response)) |
||||
|
.catch( (error) => this.onError("getIceCandidate " + error )) |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
* create RTCPeerConnection |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.createPeerConnection = function() { |
||||
|
console.log("createPeerConnection config: " + JSON.stringify(this.pcConfig)); |
||||
|
this.pc = new RTCPeerConnection(this.pcConfig); |
||||
|
var pc = this.pc; |
||||
|
pc.peerid = Math.random(); |
||||
|
|
||||
|
pc.onicecandidate = (evt) => this.onIceCandidate(evt); |
||||
|
pc.onaddstream = (evt) => this.onAddStream(evt); |
||||
|
pc.oniceconnectionstatechange = (evt) => { |
||||
|
console.log("oniceconnectionstatechange state: " + pc.iceConnectionState); |
||||
|
if (this.videoElement) { |
||||
|
if (pc.iceConnectionState === "connected") { |
||||
|
this.videoElement.style.opacity = "1.0"; |
||||
|
} |
||||
|
else if (pc.iceConnectionState === "disconnected") { |
||||
|
this.videoElement.style.opacity = "0.25"; |
||||
|
} |
||||
|
else if ( (pc.iceConnectionState === "failed") || (pc.iceConnectionState === "closed") ) { |
||||
|
this.videoElement.style.opacity = "0.5"; |
||||
|
} else if (pc.iceConnectionState === "new") { |
||||
|
this.getIceCandidate(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
pc.ondatachannel = function(evt) { |
||||
|
console.log("remote datachannel created:"+JSON.stringify(evt)); |
||||
|
|
||||
|
evt.channel.onopen = function () { |
||||
|
console.log("remote datachannel open"); |
||||
|
this.send("remote channel openned"); |
||||
|
} |
||||
|
evt.channel.onmessage = function (event) { |
||||
|
console.log("remote datachannel recv:"+JSON.stringify(event.data)); |
||||
|
} |
||||
|
} |
||||
|
pc.onicegatheringstatechange = function() { |
||||
|
if (pc.iceGatheringState === "complete") { |
||||
|
const recvs = pc.getReceivers(); |
||||
|
|
||||
|
recvs.forEach((recv) => { |
||||
|
if (recv.track && recv.track.kind === "video") { |
||||
|
console.log("codecs:" + JSON.stringify(recv.getParameters().codecs)) |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
var dataChannel = pc.createDataChannel("ClientDataChannel"); |
||||
|
dataChannel.onopen = function() { |
||||
|
console.log("local datachannel open"); |
||||
|
this.send("local channel openned"); |
||||
|
} |
||||
|
dataChannel.onmessage = function(evt) { |
||||
|
console.log("local datachannel recv:"+JSON.stringify(evt.data)); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
console.log("Cannor create datachannel error: " + e); |
||||
|
} |
||||
|
|
||||
|
console.log("Created RTCPeerConnnection with config: " + JSON.stringify(this.pcConfig) ); |
||||
|
return pc; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/* |
||||
|
* RTCPeerConnection IceCandidate callback |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.onIceCandidate = function (event) { |
||||
|
if (event.candidate) { |
||||
|
if (this.pc.currentRemoteDescription) { |
||||
|
this.addIceCandidate(this.pc.peerid, event.candidate); |
||||
|
} else { |
||||
|
this.earlyCandidates.push(event.candidate); |
||||
|
} |
||||
|
} |
||||
|
else { |
||||
|
console.log("End of candidates."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
WebRtcStreamer.prototype.addIceCandidate = function(peerid, candidate) { |
||||
|
fetch(this.srvurl + "/api/addIceCandidate?peerid="+peerid, { method: "POST", body: JSON.stringify(candidate) }) |
||||
|
.then(this._handleHttpErrors) |
||||
|
.then( (response) => (response.json()) ) |
||||
|
.then( (response) => {console.log("addIceCandidate ok:" + response)}) |
||||
|
.catch( (error) => this.onError("addIceCandidate " + error )) |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
* RTCPeerConnection AddTrack callback |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.onAddStream = function(event) { |
||||
|
console.log("Remote track added:" + JSON.stringify(event)); |
||||
|
|
||||
|
this.videoElement.srcObject = event.stream; |
||||
|
var promise = this.videoElement.play(); |
||||
|
if (promise !== undefined) { |
||||
|
promise.catch((error) => { |
||||
|
console.warn("error:"+error); |
||||
|
this.videoElement.setAttribute("controls", true); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
* AJAX /call callback |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.onReceiveCall = function(dataJson) { |
||||
|
|
||||
|
console.log("offer: " + JSON.stringify(dataJson)); |
||||
|
var descr = new RTCSessionDescription(dataJson); |
||||
|
this.pc.setRemoteDescription(descr).then(() => { |
||||
|
console.log ("setRemoteDescription ok"); |
||||
|
while (this.earlyCandidates.length) { |
||||
|
var candidate = this.earlyCandidates.shift(); |
||||
|
this.addIceCandidate(this.pc.peerid, candidate); |
||||
|
} |
||||
|
|
||||
|
this.getIceCandidate() |
||||
|
} |
||||
|
, (error) => { |
||||
|
console.log ("setRemoteDescription error:" + JSON.stringify(error)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
* AJAX /getIceCandidate callback |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.onReceiveCandidate = function(dataJson) { |
||||
|
console.log("candidate: " + JSON.stringify(dataJson)); |
||||
|
if (dataJson) { |
||||
|
for (var i=0; i<dataJson.length; i++) { |
||||
|
var candidate = new RTCIceCandidate(dataJson[i]); |
||||
|
|
||||
|
console.log("Adding ICE candidate :" + JSON.stringify(candidate) ); |
||||
|
this.pc.addIceCandidate(candidate).then( () => { console.log ("addIceCandidate OK"); } |
||||
|
, (error) => { console.log ("addIceCandidate error:" + JSON.stringify(error)); } ); |
||||
|
} |
||||
|
this.pc.addIceCandidate(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/* |
||||
|
* AJAX callback for Error |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.onError = function(status) { |
||||
|
console.log("onError:" + status); |
||||
|
} |
||||
|
|
||||
|
return WebRtcStreamer; |
||||
|
})(); |
||||
|
|
||||
|
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') { |
||||
|
window.WebRtcStreamer = WebRtcStreamer; |
||||
|
} |
||||
|
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { |
||||
|
module.exports = WebRtcStreamer; |
||||
|
} |
||||
@ -0,0 +1,313 @@ |
|||||
|
var WebRtcStreamer = (function() { |
||||
|
|
||||
|
/** |
||||
|
* Interface with WebRTC-streamer API |
||||
|
* @constructor |
||||
|
* @param {string} videoElement - id of the video element tag |
||||
|
* @param {string} srvurl - url of webrtc-streamer (default is current location) |
||||
|
*/ |
||||
|
var WebRtcStreamer = function WebRtcStreamer (videoElement, srvurl) { |
||||
|
if (typeof videoElement === "string") { |
||||
|
this.videoElement = document.getElementById(videoElement); |
||||
|
} else { |
||||
|
this.videoElement = videoElement; |
||||
|
} |
||||
|
this.srvurl = srvurl || location.protocol+"//"+window.location.hostname+":"+window.location.port; |
||||
|
this.pc = null; |
||||
|
|
||||
|
this.mediaConstraints = { offerToReceiveAudio: true, offerToReceiveVideo: true }; |
||||
|
|
||||
|
this.iceServers = null; |
||||
|
this.earlyCandidates = []; |
||||
|
} |
||||
|
|
||||
|
WebRtcStreamer.prototype._handleHttpErrors = function (response) { |
||||
|
if (!response.ok) { |
||||
|
throw Error(response.statusText); |
||||
|
} |
||||
|
return response; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Connect a WebRTC Stream to videoElement |
||||
|
* @param {string} videourl - id of WebRTC video stream |
||||
|
* @param {string} audiourl - id of WebRTC audio stream |
||||
|
* @param {string} options - options of WebRTC call |
||||
|
* @param {string} stream - local stream to send |
||||
|
* @param {string} prefmime - prefered mime |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.connect = function(videourl, audiourl, options, localstream, prefmime) { |
||||
|
this.disconnect(); |
||||
|
|
||||
|
// getIceServers is not already received
|
||||
|
if (!this.iceServers) { |
||||
|
console.log("Get IceServers"); |
||||
|
|
||||
|
fetch(this.srvurl + "/api/getIceServers") |
||||
|
.then(this._handleHttpErrors) |
||||
|
.then( (response) => (response.json()) ) |
||||
|
.then( (response) => this.onReceiveGetIceServers(response, videourl, audiourl, options, localstream, prefmime)) |
||||
|
.catch( (error) => this.onError("getIceServers " + error )) |
||||
|
|
||||
|
} else { |
||||
|
this.onReceiveGetIceServers(this.iceServers, videourl, audiourl, options, localstream, prefmime); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Disconnect a WebRTC Stream and clear videoElement source |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.disconnect = function() { |
||||
|
if (this.videoElement?.srcObject) { |
||||
|
this.videoElement.srcObject.getTracks().forEach(track => { |
||||
|
track.stop() |
||||
|
this.videoElement.srcObject.removeTrack(track); |
||||
|
}); |
||||
|
} |
||||
|
if (this.pc) { |
||||
|
fetch(this.srvurl + "/api/hangup?peerid=" + this.pc.peerid) |
||||
|
.then(this._handleHttpErrors) |
||||
|
.catch( (error) => this.onError("hangup " + error )) |
||||
|
|
||||
|
|
||||
|
try { |
||||
|
this.pc.close(); |
||||
|
} |
||||
|
catch (e) { |
||||
|
console.log ("Failure close peer connection:" + e); |
||||
|
} |
||||
|
this.pc = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
* GetIceServers callback |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.onReceiveGetIceServers = function(iceServers, videourl, audiourl, options, stream, prefmime) { |
||||
|
this.iceServers = iceServers; |
||||
|
this.pcConfig = iceServers || {"iceServers": [] }; |
||||
|
try { |
||||
|
this.createPeerConnection(); |
||||
|
|
||||
|
var callurl = this.srvurl + "/api/call?peerid=" + this.pc.peerid + "&url=" + encodeURIComponent(videourl); |
||||
|
if (audiourl) { |
||||
|
callurl += "&audiourl="+encodeURIComponent(audiourl); |
||||
|
} |
||||
|
if (options) { |
||||
|
callurl += "&options="+encodeURIComponent(options); |
||||
|
} |
||||
|
|
||||
|
if (stream) { |
||||
|
this.pc.addStream(stream); |
||||
|
} |
||||
|
|
||||
|
// clear early candidates
|
||||
|
this.earlyCandidates.length = 0; |
||||
|
|
||||
|
// create Offer
|
||||
|
this.pc.createOffer(this.mediaConstraints).then((sessionDescription) => { |
||||
|
console.log("Create offer:" + JSON.stringify(sessionDescription)); |
||||
|
|
||||
|
console.log(`video codecs:${Array.from(new Set(RTCRtpReceiver.getCapabilities("video")?.codecs?.map(codec => codec.mimeType)))}`) |
||||
|
console.log(`audio codecs:${Array.from(new Set(RTCRtpReceiver.getCapabilities("audio")?.codecs?.map(codec => codec.mimeType)))}`) |
||||
|
|
||||
|
if (prefmime != undefined) { |
||||
|
//set prefered codec
|
||||
|
const [prefkind] = prefmime.split('/'); |
||||
|
const codecs = RTCRtpReceiver.getCapabilities(prefkind).codecs; |
||||
|
const preferedCodecs = codecs.filter(codec => codec.mimeType === prefmime); |
||||
|
|
||||
|
console.log(`preferedCodecs:${JSON.stringify(preferedCodecs)}`); |
||||
|
this.pc.getTransceivers().filter(transceiver => transceiver.receiver.track.kind === prefkind).forEach(tcvr => { |
||||
|
if(tcvr.setCodecPreferences != undefined) { |
||||
|
tcvr.setCodecPreferences(preferedCodecs); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
this.pc.setLocalDescription(sessionDescription) |
||||
|
.then(() => { |
||||
|
fetch(callurl, { method: "POST", body: JSON.stringify(sessionDescription) }) |
||||
|
.then(this._handleHttpErrors) |
||||
|
.then( (response) => (response.json()) ) |
||||
|
.catch( (error) => this.onError("call " + error )) |
||||
|
.then( (response) => this.onReceiveCall(response) ) |
||||
|
.catch( (error) => this.onError("call " + error )) |
||||
|
|
||||
|
}, (error) => { |
||||
|
console.log ("setLocalDescription error:" + JSON.stringify(error)); |
||||
|
}); |
||||
|
|
||||
|
}, (error) => { |
||||
|
alert("Create offer error:" + JSON.stringify(error)); |
||||
|
}); |
||||
|
|
||||
|
} catch (e) { |
||||
|
this.disconnect(); |
||||
|
alert("connect error: " + e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
WebRtcStreamer.prototype.getIceCandidate = function() { |
||||
|
fetch(this.srvurl + "/api/getIceCandidate?peerid=" + this.pc.peerid) |
||||
|
.then(this._handleHttpErrors) |
||||
|
.then( (response) => (response.json()) ) |
||||
|
.then( (response) => this.onReceiveCandidate(response)) |
||||
|
.catch( (error) => this.onError("getIceCandidate " + error )) |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
* create RTCPeerConnection |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.createPeerConnection = function() { |
||||
|
console.log("createPeerConnection config: " + JSON.stringify(this.pcConfig)); |
||||
|
this.pc = new RTCPeerConnection(this.pcConfig); |
||||
|
var pc = this.pc; |
||||
|
pc.peerid = Math.random(); |
||||
|
|
||||
|
pc.onicecandidate = (evt) => this.onIceCandidate(evt); |
||||
|
pc.onaddstream = (evt) => this.onAddStream(evt); |
||||
|
pc.oniceconnectionstatechange = (evt) => { |
||||
|
console.log("oniceconnectionstatechange state: " + pc.iceConnectionState); |
||||
|
if (this.videoElement) { |
||||
|
if (pc.iceConnectionState === "connected") { |
||||
|
this.videoElement.style.opacity = "1.0"; |
||||
|
} |
||||
|
else if (pc.iceConnectionState === "disconnected") { |
||||
|
this.videoElement.style.opacity = "0.25"; |
||||
|
} |
||||
|
else if ( (pc.iceConnectionState === "failed") || (pc.iceConnectionState === "closed") ) { |
||||
|
this.videoElement.style.opacity = "0.5"; |
||||
|
} else if (pc.iceConnectionState === "new") { |
||||
|
this.getIceCandidate(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
pc.ondatachannel = function(evt) { |
||||
|
console.log("remote datachannel created:"+JSON.stringify(evt)); |
||||
|
|
||||
|
evt.channel.onopen = function () { |
||||
|
console.log("remote datachannel open"); |
||||
|
this.send("remote channel openned"); |
||||
|
} |
||||
|
evt.channel.onmessage = function (event) { |
||||
|
console.log("remote datachannel recv:"+JSON.stringify(event.data)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
var dataChannel = pc.createDataChannel("ClientDataChannel"); |
||||
|
dataChannel.onopen = function() { |
||||
|
console.log("local datachannel open"); |
||||
|
this.send("local channel openned"); |
||||
|
} |
||||
|
dataChannel.onmessage = function(evt) { |
||||
|
console.log("local datachannel recv:"+JSON.stringify(evt.data)); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
console.log("Cannor create datachannel error: " + e); |
||||
|
} |
||||
|
|
||||
|
console.log("Created RTCPeerConnnection with config: " + JSON.stringify(this.pcConfig) ); |
||||
|
return pc; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/* |
||||
|
* RTCPeerConnection IceCandidate callback |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.onIceCandidate = function (event) { |
||||
|
if (event.candidate) { |
||||
|
if (this.pc.currentRemoteDescription) { |
||||
|
this.addIceCandidate(this.pc.peerid, event.candidate); |
||||
|
} else { |
||||
|
this.earlyCandidates.push(event.candidate); |
||||
|
} |
||||
|
} |
||||
|
else { |
||||
|
console.log("End of candidates."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
WebRtcStreamer.prototype.addIceCandidate = function(peerid, candidate) { |
||||
|
fetch(this.srvurl + "/api/addIceCandidate?peerid="+peerid, { method: "POST", body: JSON.stringify(candidate) }) |
||||
|
.then(this._handleHttpErrors) |
||||
|
.then( (response) => (response.json()) ) |
||||
|
.then( (response) => {console.log("addIceCandidate ok:" + response)}) |
||||
|
.catch( (error) => this.onError("addIceCandidate " + error )) |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
* RTCPeerConnection AddTrack callback |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.onAddStream = function(event) { |
||||
|
console.log("Remote track added:" + JSON.stringify(event)); |
||||
|
|
||||
|
this.videoElement.srcObject = event.stream; |
||||
|
var promise = this.videoElement.play(); |
||||
|
if (promise !== undefined) { |
||||
|
promise.catch((error) => { |
||||
|
console.warn("error:"+error); |
||||
|
this.videoElement.setAttribute("controls", true); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
* AJAX /call callback |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.onReceiveCall = function(dataJson) { |
||||
|
|
||||
|
console.log("offer: " + JSON.stringify(dataJson)); |
||||
|
var descr = new RTCSessionDescription(dataJson); |
||||
|
this.pc.setRemoteDescription(descr).then(() => { |
||||
|
console.log ("setRemoteDescription ok"); |
||||
|
while (this.earlyCandidates.length) { |
||||
|
var candidate = this.earlyCandidates.shift(); |
||||
|
this.addIceCandidate(this.pc.peerid, candidate); |
||||
|
} |
||||
|
|
||||
|
this.getIceCandidate() |
||||
|
} |
||||
|
, (error) => { |
||||
|
console.log ("setRemoteDescription error:" + JSON.stringify(error)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
* AJAX /getIceCandidate callback |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.onReceiveCandidate = function(dataJson) { |
||||
|
console.log("candidate: " + JSON.stringify(dataJson)); |
||||
|
if (dataJson) { |
||||
|
for (var i=0; i<dataJson.length; i++) { |
||||
|
var candidate = new RTCIceCandidate(dataJson[i]); |
||||
|
|
||||
|
console.log("Adding ICE candidate :" + JSON.stringify(candidate) ); |
||||
|
this.pc.addIceCandidate(candidate).then( () => { console.log ("addIceCandidate OK"); } |
||||
|
, (error) => { console.log ("addIceCandidate error:" + JSON.stringify(error)); } ); |
||||
|
} |
||||
|
this.pc.addIceCandidate(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/* |
||||
|
* AJAX callback for Error |
||||
|
*/ |
||||
|
WebRtcStreamer.prototype.onError = function(status) { |
||||
|
console.log("onError:" + status); |
||||
|
} |
||||
|
|
||||
|
return WebRtcStreamer; |
||||
|
})(); |
||||
|
|
||||
|
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') { |
||||
|
window.WebRtcStreamer = WebRtcStreamer; |
||||
|
} |
||||
|
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { |
||||
|
module.exports = WebRtcStreamer; |
||||
|
} |
||||
@ -0,0 +1,317 @@ |
|||||
|
<template> |
||||
|
<div class="venue-preview"> |
||||
|
<!-- v-show="currentMarkData && currentMarkData.signPoint" --> |
||||
|
<div v-show="currentMarkData && currentMarkData.signPoint"> |
||||
|
<canvas :id="`canvasPreview${currentMarkData && currentMarkData.id}`" :width="width" :height="height" /> |
||||
|
</div> |
||||
|
<img v-if="currentMarkData && !currentMarkData.signPoint" :src="imageUrl" :onerror="defaultImg" alt=""> |
||||
|
<div id="tooltip" class="tooltip-style"> |
||||
|
<!-- <div class="tooltip-top"> |
||||
|
<h4>区域名称</h4> |
||||
|
<span class="update-time">2024-11-28 09:46</span> |
||||
|
</div> |
||||
|
<ul> |
||||
|
<li><p>在架</p><span><i>15000</i>册</span></li> |
||||
|
<li><p>错架</p><span><i>300</i>层</span> <span class="percentage">(2.00%)</span></li> |
||||
|
<li><p>错序</p><span><i>0</i>层</span><span class="percentage">(0.00%)</span></li> |
||||
|
</ul> --> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import defaultImg from '@/assets/images/system/default-img.jpg' |
||||
|
import { fabric } from 'fabric' |
||||
|
import { mapGetters } from 'vuex' |
||||
|
export default { |
||||
|
name: 'Mark', |
||||
|
props: { |
||||
|
currentMarkData: { |
||||
|
type: Object, |
||||
|
require: true, |
||||
|
default: function() { |
||||
|
return {} |
||||
|
} |
||||
|
}, |
||||
|
imageUrl: { |
||||
|
type: String, |
||||
|
default: '' |
||||
|
}, |
||||
|
pagePreview: { |
||||
|
type: String, |
||||
|
default: '' |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
defaultImg: defaultImg, |
||||
|
canvasPreview: {}, |
||||
|
width: 1200, |
||||
|
height: 600, |
||||
|
drawWidth: 2, |
||||
|
tooltipInfo: null |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
...mapGetters([ |
||||
|
'user', |
||||
|
'baseApi' |
||||
|
]) |
||||
|
}, |
||||
|
watch: { |
||||
|
width() { |
||||
|
this.canvasPreview.setWidth(this.width) |
||||
|
}, |
||||
|
height() { |
||||
|
this.canvasPreview.setHeight(this.height) |
||||
|
}, |
||||
|
currentMarkData: { |
||||
|
handler(newVal, oldVal) { |
||||
|
// 检查 newVal 是否为 null 或 undefined |
||||
|
if (!newVal) { |
||||
|
console.log('newVal is null or undefined') |
||||
|
return |
||||
|
} |
||||
|
}, |
||||
|
deep: true |
||||
|
}, |
||||
|
|
||||
|
imageUrl(newVal, oldVal) { |
||||
|
if (newVal !== oldVal) { |
||||
|
console.log('imageUrl') |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
}, |
||||
|
beforeDestroy() { |
||||
|
// if (this.canvasPreview) { |
||||
|
// this.canvasPreview.clear() |
||||
|
// this.canvasPreview.dispose() |
||||
|
// } |
||||
|
}, |
||||
|
methods: { |
||||
|
|
||||
|
initCanvasPreview(drawinfo) { |
||||
|
if (!this.currentMarkData) { |
||||
|
console.error('currentMarkData is null or undefined') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const canvasId = `canvasPreview${this.currentMarkData.id}` |
||||
|
this.canvasPreview = new fabric.Canvas(canvasId, { |
||||
|
skipTargetFind: false, |
||||
|
selectable: false, |
||||
|
selection: false |
||||
|
}) |
||||
|
|
||||
|
this.$nextTick(() => { |
||||
|
this.canvasPreview.selectionColor = 'rgba(0,0,0,0.05)' |
||||
|
this.loadDrawPreview(drawinfo) |
||||
|
this.canvasPreview.on('mouse:wheel', this.mouse) |
||||
|
}) |
||||
|
}, |
||||
|
// 鼠标滚轮放大缩小 |
||||
|
mouse(e) { |
||||
|
if (undefined === e) return |
||||
|
let zoom = (e.e.deltaY > 0 ? -0.1 : 0.1) + this.canvasPreview.getZoom() |
||||
|
zoom = Math.max(0.8, zoom) |
||||
|
// 最小为原来的1/10 |
||||
|
zoom = Math.min(3, zoom) |
||||
|
// 最大是原来的3倍 |
||||
|
const zoomPoint = new fabric.Point(e.e.pageX, e.e.pageY) |
||||
|
this.canvasPreview.zoomToPoint(zoomPoint, zoom) |
||||
|
}, |
||||
|
// 回显详情信息 |
||||
|
loadDrawPreview(drawinfo) { |
||||
|
const self = this |
||||
|
const pointGroup = drawinfo.pointInfo |
||||
|
const imgInfo = drawinfo.imgInfo |
||||
|
imgInfo.src = self.imageUrl |
||||
|
// 加载底图 |
||||
|
fabric.util.enlivenObjects([imgInfo], objects => { |
||||
|
objects.forEach(o => { |
||||
|
o.selectable = false |
||||
|
o.hasControls = false |
||||
|
o.centeredScaling = false |
||||
|
self.canvasPreview.add(o) |
||||
|
}) |
||||
|
// 处理多边形绘制回显操作 |
||||
|
pointGroup.forEach(async(item, index) => { |
||||
|
if (item.pointInfo !== '') { |
||||
|
const polygon = new fabric.Polygon(item.pointInfo, { |
||||
|
id: item.id, |
||||
|
name: item.name, |
||||
|
floorId: item.floorId, |
||||
|
rowType: item.rowType, |
||||
|
toward: item.toward, |
||||
|
floorName: item.floorName, |
||||
|
regionName: item.regionName, |
||||
|
stroke: 'rgba(196,43, 1, 1)', |
||||
|
strokeWidth: self.drawWidth, |
||||
|
fill: 'rgba(196,43, 1, 0.3)', |
||||
|
opacity: 1, |
||||
|
selectable: false, |
||||
|
hasBorders: false, |
||||
|
hasControls: false, |
||||
|
originX: 'left', // 设置原点为左上角 |
||||
|
originY: 'top' // 设置原点为左上角 |
||||
|
}) |
||||
|
self.canvasPreview.add(polygon) |
||||
|
|
||||
|
let lastClickTime = 0 |
||||
|
const doubleClickInterval = 300 |
||||
|
polygon.on('mousedown', function(e) { |
||||
|
const currentTime = new Date().getTime() |
||||
|
const timeDiff = currentTime - lastClickTime |
||||
|
|
||||
|
if (timeDiff <= doubleClickInterval) { |
||||
|
console.log('双击事件', e) |
||||
|
lastClickTime = 0 |
||||
|
const toReigonsData = { |
||||
|
id: e.target.id, |
||||
|
name: e.target.name, |
||||
|
floorId: e.target.floorId, |
||||
|
rowType: e.target.rowType, |
||||
|
toward: e.target.toward, |
||||
|
regionName: e.target.regionName, |
||||
|
floorName: e.target.floorName |
||||
|
} |
||||
|
if (self.pagePreview === 'floor') { |
||||
|
self.handleToRegions(toReigonsData) |
||||
|
} else if (self.pagePreview === 'region') { |
||||
|
self.handleToShelfs(toReigonsData) |
||||
|
} |
||||
|
} else { |
||||
|
lastClickTime = currentTime |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
polygon.on('mouseover', function(e) { |
||||
|
console.log('e', e) |
||||
|
console.log('e.target', e.target) |
||||
|
console.log('e.target.name', e.target.name) |
||||
|
|
||||
|
this.tooltipInfo = { |
||||
|
'id': e.target.id, |
||||
|
'name': e.target.name |
||||
|
} |
||||
|
// this.set({ opacity: 0.3, hoverCursor: 'pointer' }) |
||||
|
|
||||
|
if (self.pagePreview === 'floor') { |
||||
|
document.getElementById('tooltip').innerHTML = |
||||
|
`<div class="tooltip-top"> |
||||
|
<h4>${this.tooltipInfo.name}</h4> |
||||
|
<span class="update-time">2024-11-28 09:46</span> |
||||
|
</div> |
||||
|
<ul> |
||||
|
<li><p>在架</p><span><i>15000</i>册</span></li> |
||||
|
<li><p>错架</p><span><i>300</i>层</span> <span class="percentage">(2.00%)</span></li> |
||||
|
<li><p>错序</p><span><i>0</i>层</span><span class="percentage">(0.00%)</span></li> |
||||
|
</ul>` |
||||
|
} else if (self.pagePreview === 'region') { |
||||
|
document.getElementById('tooltip').innerHTML = |
||||
|
`<div class="tooltip-top"> |
||||
|
<h4>书架概况</h4> |
||||
|
<span class="update-time">2024-11-28 09:46</span> |
||||
|
</div> |
||||
|
<ul> |
||||
|
<li><p>书架</p><span><i>${this.tooltipInfo.name}</i></span></li> |
||||
|
<li><p>规则</p><span><i>双面6*8</i></span></li> |
||||
|
<li><p>在架</p><span><i>15000</i>册</span></li> |
||||
|
<li><p>错架</p><span><i>300</i>层</span> <span class="percentage">(2.00%)</span></li> |
||||
|
<li><p>错序</p><span><i>0</i>层</span><span class="percentage">(0.00%)</span></li> |
||||
|
</ul>` |
||||
|
} |
||||
|
|
||||
|
var rectLeft = e.target.left + e.target.width - 100 |
||||
|
var rectTop = e.target.top + e.target.height - 40 |
||||
|
document.getElementById('tooltip').style.left = rectLeft + 'px' |
||||
|
document.getElementById('tooltip').style.top = rectTop + 'px' |
||||
|
document.getElementById('tooltip').style.display = 'block' |
||||
|
|
||||
|
self.canvasPreview.renderAll() |
||||
|
}) |
||||
|
|
||||
|
polygon.on('mouseout', function() { |
||||
|
this.set({ opacity: 1 }) |
||||
|
document.getElementById('tooltip').style.display = 'none' |
||||
|
self.canvasPreview.renderAll() |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
self.canvasPreview.renderAll() |
||||
|
}, |
||||
|
handleToRegions(data) { |
||||
|
this.$router.push({ path: '/dataScreening/regions' }) |
||||
|
localStorage.setItem('dataScreenFloor', JSON.stringify(data)) |
||||
|
}, |
||||
|
handleToShelfs(data) { |
||||
|
this.$router.push({ path: '/dataScreening/shelf' }) |
||||
|
localStorage.setItem('dataScreenRegion', JSON.stringify(data)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
<style> |
||||
|
.tooltip-style{ |
||||
|
display:none; |
||||
|
position:absolute; |
||||
|
width: 300px; |
||||
|
background:rgba(0,0,0,.6); |
||||
|
color: #fff; |
||||
|
border-radius: 6px; |
||||
|
} |
||||
|
.tooltip-top{ |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
height: 40px; |
||||
|
line-height: 40px; |
||||
|
padding: 0 10px; |
||||
|
border-bottom: 1px solid #fff; |
||||
|
} |
||||
|
.tooltip-top span{ |
||||
|
font-size: 12px; |
||||
|
|
||||
|
} |
||||
|
#tooltip ul{ |
||||
|
padding: 10px; |
||||
|
} |
||||
|
#tooltip ul li{ |
||||
|
display: flex; |
||||
|
justify-content: flex-start; |
||||
|
align-items: center; |
||||
|
line-height: 36px; |
||||
|
font-style: normal; |
||||
|
} |
||||
|
|
||||
|
#tooltip ul li p{ |
||||
|
width: 80px; |
||||
|
font-weight: bold; |
||||
|
text-align: right; |
||||
|
} |
||||
|
#tooltip ul li span{ |
||||
|
width: 100px; |
||||
|
display: block; |
||||
|
text-align: right; |
||||
|
} |
||||
|
#tooltip ul li i{ |
||||
|
font-style: normal; |
||||
|
font-weight: bold; |
||||
|
padding: 0 10px; |
||||
|
color: #0348f3; |
||||
|
} |
||||
|
#tooltip ul li span.percentage{ |
||||
|
width: auto; |
||||
|
} |
||||
|
</style> |
||||
|
<style lang="scss" scoped> |
||||
|
#expImg{ |
||||
|
display: none; |
||||
|
} |
||||
|
.venue-preview{ |
||||
|
background-color: #e8f2ff; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,61 @@ |
|||||
|
<template> |
||||
|
<div style="width: 100%; height: 100%; overflow: hidden;"> |
||||
|
<!-- <h1>海康威视视频流</h1> --> |
||||
|
<!-- <h3>400W</h3> |
||||
|
<VideoCom :rtsp="rtspurl1" ref="video1"></VideoCom> |
||||
|
<h3>800W</h3> |
||||
|
<VideoCom :rtsp="rtspurl2" ref="video2"></VideoCom> --> |
||||
|
|
||||
|
<video id="video" controls autoplay muted width="100%" height="100%" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
// import VideoCom from '../components/video' |
||||
|
// rtsp://admin:ftzn83560792@192.168.99.23:554/h264/ch1/main/av_stream |
||||
|
// rtsp://admin:ftzn83560792@192.168.99.26:554/Streaming/Channels/101 |
||||
|
export default { |
||||
|
name: 'Home', |
||||
|
components: { |
||||
|
// VideoCom |
||||
|
}, |
||||
|
props: { |
||||
|
hkConfig: { |
||||
|
type: Object, |
||||
|
required: true |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
webRtcServer: null, |
||||
|
camera_ip: '127.0.0.1:8000' |
||||
|
} |
||||
|
}, |
||||
|
created() { |
||||
|
// this.controlVolume() |
||||
|
}, |
||||
|
mounted() { |
||||
|
// video:需要绑定的video控件ID |
||||
|
// 127.0.0.1:8000:启动webrtc-streamer的设备IP和端口,默认8000 |
||||
|
this.webRtcServer = new WebRtcStreamer('video', location.protocol + '//' + this.camera_ip) |
||||
|
// 需要查看的rtsp地址,根据自己的摄像头传入对应的rtsp地址即可。注意:视频编码格式必须是H264的,否则无法正常显示,编码格式可在摄像头的后台更改 |
||||
|
this.webRtcServer.connect('rtsp://' + this.hkConfig.username + ':' + this.hkConfig.password + '@' + this.hkConfig.ip + ':' + this.hkConfig.port + '/h264/ch1/main/av_stream') |
||||
|
}, |
||||
|
// 销毁视频流 |
||||
|
beforeDestroy() { |
||||
|
this.webRtcServer.disconnect() |
||||
|
this.webRtcServer = null |
||||
|
}, |
||||
|
methods: { |
||||
|
controlVolume() { |
||||
|
this.$nextTick(() => { |
||||
|
this.$refs.video1.volume = 0 |
||||
|
this.$refs.video2.volume = 0 |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
</style> |
||||
@ -0,0 +1,293 @@ |
|||||
|
<template> |
||||
|
<div class="app-container"> |
||||
|
<div class="venue-header"> |
||||
|
<h4><i class="iconfont icon-shuju" />架位总览</h4> |
||||
|
<p><i class="iconfont icon-gongsi" />{{ user.fonds.fondsName }}</p> |
||||
|
</div> |
||||
|
<div class="venue-content"> |
||||
|
<crudOperation :permission="permission"> |
||||
|
<template v-slot:middle> |
||||
|
<el-button v-permission="permission.add" size="mini" @click="crud.toAdd"> |
||||
|
<i class="iconfont icon-shengchengpandiandan" /> |
||||
|
架位盘点 |
||||
|
</el-button> |
||||
|
</template> |
||||
|
<template v-slot:right> |
||||
|
<el-button :loading="crud.downloadLoading" size="mini" @click="doExport(crud.selections)"> |
||||
|
<i class="iconfont icon-daochu" /> |
||||
|
导出 |
||||
|
</el-button> |
||||
|
</template> |
||||
|
</crudOperation> |
||||
|
<div class="venue-left"> |
||||
|
<div class="container-right tab-content"> |
||||
|
<span class="right-top-line" /> |
||||
|
<span class="left-bottom-line" /> |
||||
|
<ul class="tab-nav"> |
||||
|
<li v-for="(item,index) in floorOptions" :key="index" :class="{ 'active-tab-nav': tabIndex == index }" @click="changeActiveTab(index)">{{ item.floorName }}<i /></li> |
||||
|
<!-- 最右侧装饰img --> |
||||
|
<span class="tab-right-img" /> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="venue-right" /> |
||||
|
|
||||
|
<!-- form --> |
||||
|
<el-dialog append-to-body :close-on-click-modal="false" :modal-append-to-body="false" :before-close="crud.cancelCU" :visible="crud.status.cu > 0" :title="crud.status.title"> |
||||
|
<span class="dialog-right-top" /> |
||||
|
<span class="dialog-left-bottom" /> |
||||
|
<div class="setting-dialog"> |
||||
|
<el-form ref="form" :inline="true" :model="form" :rules="rules" size="small" label-width="80px"> |
||||
|
<el-form-item label="盘点单号" prop="taskName"> |
||||
|
<el-input v-model="form.taskName" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="盘点类型" prop="taskType"> |
||||
|
<el-input v-model="form.taskType" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="目标位置" prop="location"> |
||||
|
<el-input v-model="form.location" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="目标数量" prop="number"> |
||||
|
<el-input v-model="form.number" /> |
||||
|
</el-form-item> |
||||
|
<el-row> |
||||
|
<el-form-item label="备注" prop="remark"> |
||||
|
<el-input v-model="form.remark" type="textarea" style="width: 572px;" :rows="4" /> |
||||
|
</el-form-item> |
||||
|
</el-row> |
||||
|
</el-form> |
||||
|
<div slot="footer" class="dialog-footer"> |
||||
|
<el-button type="text" @click="crud.cancelCU">取消</el-button> |
||||
|
<el-button :loading="crud.status.cu === 2" type="primary" @click="crud.submitCU">保存</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
</div> |
||||
|
</div></template> |
||||
|
|
||||
|
<script> |
||||
|
import { FetchLibraryFloorListAll } from '@/api/floor/index' |
||||
|
import { FetchInitLibraryRegionList } from '@/api/area/index' |
||||
|
import crudRegion from '@/api/area/index' |
||||
|
import CRUD, { presenter, header, form, crud } from '@crud/crud' |
||||
|
import crudOperation from '@crud/CRUD.operation' |
||||
|
import { mapGetters } from 'vuex' |
||||
|
import defaultImg from '@/assets/images/system/default-img.jpg' |
||||
|
|
||||
|
const defaultForm = { id: null, taskType: null, taskName: null, location: null, number: null, remark: null } |
||||
|
export default { |
||||
|
name: 'DataScreening', |
||||
|
components: { crudOperation }, |
||||
|
cruds() { |
||||
|
return CRUD({ title: '数据总览', url: 'api/libraryRegion/initLibraryRegionList', crudMethod: { ...crudRegion }, sort: [], optShow: { |
||||
|
add: false, |
||||
|
edit: false, |
||||
|
del: false, |
||||
|
download: false, |
||||
|
group: false, |
||||
|
reset: false |
||||
|
}}) |
||||
|
}, |
||||
|
mixins: [presenter(), header(), form(defaultForm), crud()], |
||||
|
data() { |
||||
|
const _this = this |
||||
|
return { |
||||
|
floorOptions: [], |
||||
|
tabIndex: 0, |
||||
|
defaultImg: defaultImg, |
||||
|
imageUrl: defaultImg, |
||||
|
imageRegionUrl: defaultImg, |
||||
|
currentMarkData: null, |
||||
|
allCoverData: [], |
||||
|
swiperActiveIndex: 0, |
||||
|
rightDataIndex: null, |
||||
|
swiperOptionContent: { |
||||
|
slidesPerView: 'auto', |
||||
|
on: { |
||||
|
slideChangeTransitionStart: function() { |
||||
|
_this.rightDataIndex = null |
||||
|
_this.swiperActiveIndex = this.activeIndex |
||||
|
_this.swiperTitle.slideTo(this.activeIndex, 500, false) |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
swiperOptionTitle: { |
||||
|
slidesPerView: 'auto', |
||||
|
freeMode: true |
||||
|
}, |
||||
|
tabListData: [{ name: '热门图书' }, { name: '热门架位' }, { name: '冷面图书' }], |
||||
|
permission: { |
||||
|
add: ['admin', 'floor:add'], |
||||
|
edit: ['admin', 'floor:edit'], |
||||
|
del: ['admin', 'floor:del'] |
||||
|
}, |
||||
|
rules: { |
||||
|
taskName: [ |
||||
|
{ required: true, message: '请输入盘点单号', trigger: 'blur' } |
||||
|
], |
||||
|
taskType: [ |
||||
|
{ required: true, message: '请输入盘点类型', trigger: 'blur' } |
||||
|
], |
||||
|
location: [ |
||||
|
{ required: true, message: '请输入目标位置', trigger: 'blur' } |
||||
|
], |
||||
|
number: [ |
||||
|
{ required: true, message: '请输入目标数量', trigger: 'blur' } |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
...mapGetters([ |
||||
|
'user', |
||||
|
'baseApi' |
||||
|
]), |
||||
|
swiperContent() { |
||||
|
return this.$refs.swiperContent.$el.swiper |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
[CRUD.HOOK.beforeRefresh]() { |
||||
|
this.getLibraryFloorListAll() |
||||
|
}, |
||||
|
[CRUD.HOOK.afterRefresh](crud) { |
||||
|
}, |
||||
|
// 提交前的验证 |
||||
|
[CRUD.HOOK.afterValidateCU](crud) { |
||||
|
return true |
||||
|
}, |
||||
|
// 获取楼层数据 |
||||
|
getLibraryFloorListAll() { |
||||
|
FetchLibraryFloorListAll().then(res => { |
||||
|
this.floorOptions = res |
||||
|
this.changeActiveTab(this.tabIndex) |
||||
|
}).catch(() => { |
||||
|
}) |
||||
|
}, |
||||
|
changeActiveTab(index) { |
||||
|
this.tabIndex = index |
||||
|
const params = { |
||||
|
'floorId': this.floorOptions[index].id |
||||
|
} |
||||
|
FetchInitLibraryRegionList(params).then(res => { |
||||
|
console.log(res) |
||||
|
this.allCoverData = res.content |
||||
|
|
||||
|
if (this.allCoverData.length !== 0) { |
||||
|
this.currentMarkData = this.allCoverData[0] |
||||
|
if (this.allCoverData[0].floorMap) { |
||||
|
this.imageUrl = this.baseApi + '/api/fileRelevant/getImg?imgId=' + this.allCoverData[0].floorMap |
||||
|
} else { |
||||
|
this.imageUrl = this.defaultImg |
||||
|
} |
||||
|
if (this.tabIndex === 0) { |
||||
|
if (this.allCoverData[0].signPoint) { |
||||
|
const drawinfo = JSON.parse(this.allCoverData[0].signPoint) |
||||
|
this.$nextTick(() => { |
||||
|
this.$refs.previewRefs.initCanvasPreview(drawinfo) |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
this.currentMarkData = {} |
||||
|
if (this.floorOptions[index].floorMap) { |
||||
|
this.imageUrl = this.baseApi + '/api/fileRelevant/getImg?imgId=' + this.floorOptions[index].floorMap |
||||
|
} else { |
||||
|
this.imageUrl = this.defaultImg |
||||
|
} |
||||
|
} |
||||
|
}).catch(() => { |
||||
|
}) |
||||
|
}, |
||||
|
handleSlidClickFun(index) { |
||||
|
this.rightDataIndex = null |
||||
|
this.handleSlideToFun(index) |
||||
|
}, |
||||
|
handleSlideToFun(index) { |
||||
|
this.swiperActiveIndex = index |
||||
|
this.swiperContent.slideTo(index, 500, false) |
||||
|
this.swiperTitle.slideTo(index, 500, false) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.container-right{ |
||||
|
min-height: calc(100vh - 232px) !important; |
||||
|
} |
||||
|
.venue-content{ |
||||
|
position: relative; |
||||
|
} |
||||
|
.crud-opts{ |
||||
|
position: absolute; |
||||
|
right: 20px; |
||||
|
top: 10px; |
||||
|
} |
||||
|
.venue-left{ |
||||
|
flex: 1; |
||||
|
margin-right: 0 !important; |
||||
|
.venue-preview{ |
||||
|
height: 633px !important; |
||||
|
} |
||||
|
} |
||||
|
.venue-right{ |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
width: 400px; |
||||
|
padding: 50px 10px 20px 10px !important; |
||||
|
.lib-right-item{ |
||||
|
position: relative; |
||||
|
// padding: 10px; |
||||
|
// height: calc(100% / 3); |
||||
|
padding-bottom: 10px; |
||||
|
margin-bottom: 10px; |
||||
|
border: 1px solid #E8F2FF; |
||||
|
border-radius: 4px; |
||||
|
h4{ |
||||
|
padding: 6px 10px; |
||||
|
background-color: #E8F2FF; |
||||
|
color: #000; |
||||
|
line-height: 30px; |
||||
|
border-bottom: 1px solid #edeff3; |
||||
|
} |
||||
|
.refresh-date{ |
||||
|
position: absolute; |
||||
|
right: 14px; |
||||
|
top: 10px; |
||||
|
font-size: 12px; |
||||
|
line-height: 30px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.data-right-list { |
||||
|
padding-top: 10px; |
||||
|
li{ |
||||
|
display: flex; |
||||
|
justify-content: flex-start; |
||||
|
align-items: center; |
||||
|
line-height: 36px; |
||||
|
|
||||
|
p{ |
||||
|
width: 80px; |
||||
|
font-weight: bold; |
||||
|
text-align: right; |
||||
|
} |
||||
|
span{ |
||||
|
width: 140px; |
||||
|
display: block; |
||||
|
text-align: right; |
||||
|
i{ |
||||
|
font-style: normal; |
||||
|
font-weight: bold; |
||||
|
padding: 0 10px; |
||||
|
color: #0348f3; |
||||
|
} |
||||
|
&.percentage{ |
||||
|
width: auto; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,331 @@ |
|||||
|
<template> |
||||
|
<div class="app-container"> |
||||
|
<div class="venue-header"> |
||||
|
<h4 @click="handleToShelfs"><i class="iconfont icon-shuju" />区域总览</h4> |
||||
|
<p><i class="iconfont icon-gongsi" />{{ user.fonds.fondsName }}</p> |
||||
|
</div> |
||||
|
<div class="venue-content"> |
||||
|
<crudOperation :permission="permission"> |
||||
|
<template v-slot:middle> |
||||
|
<el-button v-permission="permission.add" size="mini" @click="crud.toAdd"> |
||||
|
<i class="iconfont icon-shengchengpandiandan" /> |
||||
|
区域盘点 |
||||
|
</el-button> |
||||
|
</template> |
||||
|
<template v-slot:right> |
||||
|
<el-button :loading="crud.downloadLoading" size="mini" @click="doExport(crud.selections)"> |
||||
|
<i class="iconfont icon-daochu" /> |
||||
|
导出 |
||||
|
</el-button> |
||||
|
</template> |
||||
|
</crudOperation> |
||||
|
<div class="venue-left"> |
||||
|
<div class="container-right tab-content"> |
||||
|
<span class="right-top-line" /> |
||||
|
<span class="left-bottom-line" /> |
||||
|
<ul class="tab-nav"> |
||||
|
<li v-for="(item,index) in regionOptions" :key="index" :class="{ 'active-tab-nav': tabIndex == index }" @click="changeActiveTab(index)">{{ item.regionName }}<i /></li> |
||||
|
<!-- 最右侧装饰img --> |
||||
|
<span class="tab-right-img" /> |
||||
|
</ul> |
||||
|
<CanvasPreview ref="previewRefs" page-preview="region" :current-mark-data="currentMarkData" :image-url="imageUrl" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="venue-right"> |
||||
|
<div class="lib-right-item lib-info"> |
||||
|
<h4>本区概况</h4> |
||||
|
<ul class="data-right-list"> |
||||
|
<li><p>书架</p><span><i>100</i>层</span></li> |
||||
|
<li><p>摄像头</p><span><i>300</i>层</span></li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
<div class="lib-right-item"> |
||||
|
<h4>本区盘点概况</h4> |
||||
|
<div class="refresh-date">2024-11-28 09:46</div> |
||||
|
<ul class="data-right-list"> |
||||
|
<li><p>在架</p><span><i>15000</i>册</span></li> |
||||
|
<li><p>错架</p><span><i>300</i>层</span> <span class="percentage">(2.00%)</span></li> |
||||
|
<li><p>错序</p><span><i>0</i>层</span><span class="percentage">(0.00%)</span></li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
<div class="lib-right-item"> |
||||
|
<h4>本区流通统计</h4> |
||||
|
<div class="refresh-date">2024-11-28 09:46</div> |
||||
|
<bookSwiper /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- form --> |
||||
|
<el-dialog append-to-body :close-on-click-modal="false" :modal-append-to-body="false" :before-close="crud.cancelCU" :visible="crud.status.cu > 0" :title="crud.status.title"> |
||||
|
<span class="dialog-right-top" /> |
||||
|
<span class="dialog-left-bottom" /> |
||||
|
<div class="setting-dialog"> |
||||
|
<el-form ref="form" :inline="true" :model="form" :rules="rules" size="small" label-width="80px"> |
||||
|
<el-form-item label="盘点单号" prop="taskName"> |
||||
|
<el-input v-model="form.taskName" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="盘点类型" prop="taskType"> |
||||
|
<el-input v-model="form.taskType" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="目标位置" prop="location"> |
||||
|
<el-input v-model="form.location" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="目标数量" prop="number"> |
||||
|
<el-input v-model="form.number" /> |
||||
|
</el-form-item> |
||||
|
<el-row> |
||||
|
<el-form-item label="备注" prop="remark"> |
||||
|
<el-input v-model="form.remark" type="textarea" style="width: 572px;" :rows="4" /> |
||||
|
</el-form-item> |
||||
|
</el-row> |
||||
|
</el-form> |
||||
|
<div slot="footer" class="dialog-footer"> |
||||
|
<el-button type="text" @click="crud.cancelCU">取消</el-button> |
||||
|
<el-button :loading="crud.status.cu === 2" type="primary" @click="crud.submitCU">保存</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import crudRegion from '@/api/area/index' |
||||
|
import { FetchInitBookShelfList } from '@/api/shelf/index' |
||||
|
import CRUD, { presenter, header, form, crud } from '@crud/crud' |
||||
|
import crudOperation from '@crud/CRUD.operation' |
||||
|
import { mapGetters } from 'vuex' |
||||
|
import defaultImg from '@/assets/images/system/default-img.jpg' |
||||
|
import bookSwiper from '@/views/components/bookSwiper.vue' |
||||
|
import CanvasPreview from '@/views/components/canvasPreview.vue' |
||||
|
|
||||
|
const defaultForm = { id: null, taskType: null, taskName: null, location: null, number: null, remark: null } |
||||
|
export default { |
||||
|
name: 'DataScreening', |
||||
|
components: { crudOperation, bookSwiper, CanvasPreview }, |
||||
|
cruds() { |
||||
|
return CRUD({ title: '区域总览', url: 'api/libraryRegion/initLibraryRegionList', crudMethod: { ...crudRegion }, sort: [], optShow: { |
||||
|
add: false, |
||||
|
edit: false, |
||||
|
del: false, |
||||
|
download: false, |
||||
|
group: false, |
||||
|
reset: false |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
mixins: [presenter(), header(), form(defaultForm), crud()], |
||||
|
data() { |
||||
|
const _this = this |
||||
|
return { |
||||
|
regionOptions: [], |
||||
|
currentRegionId: null, |
||||
|
tabIndex: 0, |
||||
|
defaultImg: defaultImg, |
||||
|
imageUrl: defaultImg, |
||||
|
imageRegionUrl: defaultImg, |
||||
|
currentMarkData: null, |
||||
|
allCoverData: [], |
||||
|
swiperActiveIndex: 0, |
||||
|
rightDataIndex: null, |
||||
|
swiperOptionContent: { |
||||
|
slidesPerView: 'auto', |
||||
|
on: { |
||||
|
slideChangeTransitionStart: function() { |
||||
|
_this.rightDataIndex = null |
||||
|
_this.swiperActiveIndex = this.activeIndex |
||||
|
_this.swiperTitle.slideTo(this.activeIndex, 500, false) |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
swiperOptionTitle: { |
||||
|
slidesPerView: 'auto', |
||||
|
freeMode: true |
||||
|
}, |
||||
|
tabListData: [{ name: '热门图书' }, { name: '热门架位' }, { name: '冷面图书' }], |
||||
|
permission: { |
||||
|
add: ['admin', 'floor:add'], |
||||
|
edit: ['admin', 'floor:edit'], |
||||
|
del: ['admin', 'floor:del'] |
||||
|
}, |
||||
|
rules: { |
||||
|
taskName: [ |
||||
|
{ required: true, message: '请输入盘点单号', trigger: 'blur' } |
||||
|
], |
||||
|
taskType: [ |
||||
|
{ required: true, message: '请输入盘点类型', trigger: 'blur' } |
||||
|
], |
||||
|
location: [ |
||||
|
{ required: true, message: '请输入目标位置', trigger: 'blur' } |
||||
|
], |
||||
|
number: [ |
||||
|
{ required: true, message: '请输入目标数量', trigger: 'blur' } |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
...mapGetters([ |
||||
|
'user', |
||||
|
'baseApi' |
||||
|
]), |
||||
|
swiperContent() { |
||||
|
return this.$refs.swiperContent.$el.swiper |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
handleToShelfs() { |
||||
|
this.$router.push({ path: '/dataScreening/shelf', query: { }}) |
||||
|
}, |
||||
|
[CRUD.HOOK.beforeRefresh]() { |
||||
|
const data = JSON.parse(localStorage.getItem('dataScreenFloor')) |
||||
|
this.crud.query.floorId = data.floorId |
||||
|
this.currentRegionId = data.id |
||||
|
}, |
||||
|
[CRUD.HOOK.afterRefresh](crud) { |
||||
|
this.regionOptions = crud.data |
||||
|
const item = this.regionOptions.find(element => element.id === this.currentRegionId) |
||||
|
const index = item ? this.regionOptions.indexOf(item) : -1 |
||||
|
this.tabIndex = index |
||||
|
this.changeActiveTab(index) |
||||
|
}, |
||||
|
// 提交前的验证 |
||||
|
[CRUD.HOOK.afterValidateCU](crud) { |
||||
|
return true |
||||
|
}, |
||||
|
changeActiveTab(index) { |
||||
|
this.tabIndex = index |
||||
|
const params = { |
||||
|
'floorId': this.regionOptions[index].floorId, |
||||
|
'regionId': this.regionOptions[index].id |
||||
|
} |
||||
|
FetchInitBookShelfList(params).then(res => { |
||||
|
console.log(res) |
||||
|
this.allCoverData = res.content |
||||
|
if (this.regionOptions[index].regionMap) { |
||||
|
this.imageUrl = this.baseApi + '/api/fileRelevant/getImg?imgId=' + this.regionOptions[index].regionMap |
||||
|
} else { |
||||
|
this.imageUrl = this.defaultImg |
||||
|
} |
||||
|
|
||||
|
if (this.allCoverData.length !== 0) { |
||||
|
this.currentMarkData = this.regionOptions[index] |
||||
|
const signPoint = this.allCoverData.find(item => item.signPoint !== null)?.signPoint |
||||
|
const imgInfo = signPoint ? JSON.parse(signPoint).imgInfo : null |
||||
|
const parsedSignPoints = this.allCoverData.map(item => { |
||||
|
const signPoint = item.signPoint ? JSON.parse(item.signPoint) : null |
||||
|
return { |
||||
|
id: item.shelfId, |
||||
|
name: item.shelfName, |
||||
|
rowType: item.rowType, |
||||
|
toward: item.toward, |
||||
|
floorName: item.floorName, |
||||
|
regionName: item.regionName, |
||||
|
pointInfo: signPoint ? signPoint.pointInfo[0].pointInfo : null |
||||
|
} |
||||
|
}) |
||||
|
const result = { |
||||
|
pointInfo: parsedSignPoints, |
||||
|
imgInfo: imgInfo |
||||
|
} |
||||
|
this.$nextTick(() => { |
||||
|
this.$refs.previewRefs.initCanvasPreview(result) |
||||
|
}) |
||||
|
} else { |
||||
|
this.currentMarkData = {} |
||||
|
} |
||||
|
}).catch(() => { |
||||
|
}) |
||||
|
}, |
||||
|
handleSlidClickFun(index) { |
||||
|
this.rightDataIndex = null |
||||
|
this.handleSlideToFun(index) |
||||
|
}, |
||||
|
handleSlideToFun(index) { |
||||
|
this.swiperActiveIndex = index |
||||
|
this.swiperContent.slideTo(index, 500, false) |
||||
|
this.swiperTitle.slideTo(index, 500, false) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.container-right{ |
||||
|
min-height: calc(100vh - 232px) !important; |
||||
|
} |
||||
|
.venue-content{ |
||||
|
position: relative; |
||||
|
} |
||||
|
.crud-opts{ |
||||
|
position: absolute; |
||||
|
right: 20px; |
||||
|
top: 10px; |
||||
|
} |
||||
|
.venue-left{ |
||||
|
flex: 1; |
||||
|
margin-right: 0 !important; |
||||
|
.venue-preview{ |
||||
|
height: 633px !important; |
||||
|
} |
||||
|
} |
||||
|
.venue-right{ |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
width: 400px; |
||||
|
padding: 50px 10px 20px 10px !important; |
||||
|
.lib-right-item{ |
||||
|
position: relative; |
||||
|
// padding: 10px; |
||||
|
// height: calc(100% / 3); |
||||
|
padding-bottom: 10px; |
||||
|
margin-bottom: 10px; |
||||
|
border: 1px solid #E8F2FF; |
||||
|
border-radius: 4px; |
||||
|
h4{ |
||||
|
padding: 6px 10px; |
||||
|
background-color: #E8F2FF; |
||||
|
color: #000; |
||||
|
line-height: 30px; |
||||
|
border-bottom: 1px solid #edeff3; |
||||
|
} |
||||
|
.refresh-date{ |
||||
|
position: absolute; |
||||
|
right: 14px; |
||||
|
top: 10px; |
||||
|
font-size: 12px; |
||||
|
line-height: 30px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.data-right-list { |
||||
|
padding-top: 10px; |
||||
|
li{ |
||||
|
display: flex; |
||||
|
justify-content: flex-start; |
||||
|
align-items: center; |
||||
|
line-height: 36px; |
||||
|
|
||||
|
p{ |
||||
|
width: 80px; |
||||
|
font-weight: bold; |
||||
|
text-align: right; |
||||
|
} |
||||
|
span{ |
||||
|
width: 140px; |
||||
|
display: block; |
||||
|
text-align: right; |
||||
|
i{ |
||||
|
font-style: normal; |
||||
|
font-weight: bold; |
||||
|
padding: 0 10px; |
||||
|
color: #0348f3; |
||||
|
} |
||||
|
&.percentage{ |
||||
|
width: auto; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,672 @@ |
|||||
|
<template> |
||||
|
<div class="app-container"> |
||||
|
<div class="venue-header"> |
||||
|
<h4 @click="handleToGrids"><i class="iconfont icon-shuju" />书架总览</h4> |
||||
|
<span class="bookshelf-area">{{ floorName }} - {{ regionName }}</span> |
||||
|
<p><i class="iconfont icon-gongsi" />{{ user.fonds.fondsName }}</p> |
||||
|
</div> |
||||
|
<div class="venue-content"> |
||||
|
<crudOperation :permission="permission"> |
||||
|
<template v-slot:middle> |
||||
|
<el-select v-model="layerVal" clearable size="small" placeholder="楼层" class="filter-item" style="width: 100px; margin-right: 20px;"> |
||||
|
<el-option v-for="item in layerOptions" :key="item.id" :label="item.name" :value="item.id" /> |
||||
|
</el-select> |
||||
|
<el-button v-permission="permission.add" size="mini" @click="crud.toAdd"> |
||||
|
<i class="iconfont icon-shengchengpandiandan" /> |
||||
|
书架盘点 |
||||
|
</el-button> |
||||
|
</template> |
||||
|
<template v-slot:right> |
||||
|
<el-button :loading="crud.downloadLoading" size="mini" @click="doExport(crud.selections)"> |
||||
|
<i class="iconfont icon-daochu" /> |
||||
|
导出 |
||||
|
</el-button> |
||||
|
</template> |
||||
|
</crudOperation> |
||||
|
<div class="venue-left"> |
||||
|
<div class="container-right tab-content"> |
||||
|
<span class="right-top-line" /> |
||||
|
<span class="left-bottom-line" /> |
||||
|
|
||||
|
<!-- <div style="display: flex;justify-content: flex-start; align-items: center;"> --> |
||||
|
<swiper |
||||
|
ref="swiperTitle" |
||||
|
class="swiper-title" |
||||
|
:options="swiperOptionTitle" |
||||
|
:auto-update="true" |
||||
|
:auto-destroy="true" |
||||
|
:delete-instance-on-destroy="true" |
||||
|
:cleanup-styles-on-destroy="true" |
||||
|
> |
||||
|
<swiper-slide |
||||
|
v-for="(item, index) of tabListData" |
||||
|
ref="swiperSlideItem" |
||||
|
:key="'name' + index" |
||||
|
:iname="item.name" |
||||
|
class="swiper-slide-title" |
||||
|
> |
||||
|
<div |
||||
|
class="tab-name" |
||||
|
:class="{ active: index === swiperActiveIndex }" |
||||
|
@click="handleSlidClickFun(index)" |
||||
|
> |
||||
|
{{ item.name }} |
||||
|
</div> |
||||
|
</swiper-slide> |
||||
|
</swiper> |
||||
|
<div class="tag-info"> |
||||
|
<p>错序:1</p> |
||||
|
<p>错架:1</p> |
||||
|
<p>在架:20</p> |
||||
|
</div> |
||||
|
<!-- </div> --> |
||||
|
<swiper |
||||
|
ref="swiperContent" |
||||
|
class="swiper-content" |
||||
|
:options="swiperOptionContent" |
||||
|
:auto-update="true" |
||||
|
:auto-destroy="true" |
||||
|
:delete-instance-on-destroy="true" |
||||
|
:cleanup-styles-on-destroy="true" |
||||
|
> |
||||
|
<swiper-slide |
||||
|
v-for="(item, index) of tabListData" |
||||
|
:key="'content' + index" |
||||
|
class="swiper-slide-content" |
||||
|
> |
||||
|
|
||||
|
<ul class="cabinet-row"> |
||||
|
<li |
||||
|
v-for="(cell,i) in booShelfGrid" |
||||
|
:key="i" |
||||
|
class="cabinet-cell" |
||||
|
:style="cellStyle" |
||||
|
:class="{ active: i === cellIndex }" |
||||
|
@click="handleCellCurrent(cell,i)" |
||||
|
@mouseenter="showPopover(i)" |
||||
|
@mouseleave="hidePopover" |
||||
|
> |
||||
|
<span class="cell-name">{{ removeAreaPrefix(cell.gridName) }}</span> |
||||
|
<!-- v-if="popoverIndex === i" --> |
||||
|
<el-popover |
||||
|
v-if="popoverIndex === i" |
||||
|
ref="popover" |
||||
|
:visible="popoverVisible[i]" |
||||
|
width="400" |
||||
|
:style="popoverStyles[i]" |
||||
|
trigger="manual" |
||||
|
> |
||||
|
<div slot="reference" class="popover-content"> |
||||
|
<div class="tooltip-top"> |
||||
|
<h4>层位概况</h4> |
||||
|
<i class="update-time">2024-11-28 09:46</i> |
||||
|
</div> |
||||
|
<ul> |
||||
|
<li><p>层位</p><em class="percentage"><i style="color: #fff;">{{ removeAreaPrefix(cell.gridName) }}</i></em></li> |
||||
|
<li><p>在架</p><em><i>15000</i>册</em></li> |
||||
|
<li><p>错架</p><em><i>300</i>层</em> <em class="percentage">(2.00%)</em></li> |
||||
|
<li><p>错序</p><em><i>0</i>层</em><em class="percentage">(0.00%)</em></li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</el-popover> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</swiper-slide> |
||||
|
</swiper> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="venue-right"> |
||||
|
<div class="lib-right-item lib-info"> |
||||
|
<h4>本架概况</h4> |
||||
|
<ul class="data-right-list"> |
||||
|
<li><p>书架</p><span><i>001排</i></span></li> |
||||
|
<li><p>规则</p><span><i>双面 6 x 5</i></span></li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
<div class="lib-right-item"> |
||||
|
<h4>本架盘点概况</h4> |
||||
|
<div class="refresh-date">2024-11-28 09:46</div> |
||||
|
<ul class="data-right-list"> |
||||
|
<li><p>在架</p><span><i>15000</i>册</span></li> |
||||
|
<li><p>错架</p><span><i>300</i>层</span> <span class="percentage">(2.00%)</span></li> |
||||
|
<li><p>错序</p><span><i>0</i>层</span><span class="percentage">(0.00%)</span></li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- form --> |
||||
|
<el-dialog append-to-body :close-on-click-modal="false" :modal-append-to-body="false" :before-close="crud.cancelCU" :visible="crud.status.cu > 0" :title="crud.status.title"> |
||||
|
<span class="dialog-right-top" /> |
||||
|
<span class="dialog-left-bottom" /> |
||||
|
<div class="setting-dialog"> |
||||
|
<el-form ref="form" :inline="true" :model="form" :rules="rules" size="small" label-width="80px"> |
||||
|
<el-form-item label="盘点单号" prop="taskName"> |
||||
|
<el-input v-model="form.taskName" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="盘点类型" prop="taskType"> |
||||
|
<el-input v-model="form.taskType" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="目标位置" prop="location"> |
||||
|
<el-input v-model="form.location" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="目标数量" prop="number"> |
||||
|
<el-input v-model="form.number" /> |
||||
|
</el-form-item> |
||||
|
<el-row> |
||||
|
<el-form-item label="备注" prop="remark"> |
||||
|
<el-input v-model="form.remark" type="textarea" style="width: 572px;" :rows="4" /> |
||||
|
</el-form-item> |
||||
|
</el-row> |
||||
|
</el-form> |
||||
|
<div slot="footer" class="dialog-footer"> |
||||
|
<el-button type="text" @click="crud.cancelCU">取消</el-button> |
||||
|
<el-button :loading="crud.status.cu === 2" type="primary" @click="crud.submitCU">保存</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { FetchInitShelfGridByShelfId, FetchBookShelfDetails } from '@/api/shelf/index' |
||||
|
import crudRegion from '@/api/area/index' |
||||
|
import CRUD, { presenter, header, form, crud } from '@crud/crud' |
||||
|
import crudOperation from '@crud/CRUD.operation' |
||||
|
import { mapGetters } from 'vuex' |
||||
|
import { swiper, swiperSlide } from 'vue-awesome-swiper' |
||||
|
import 'swiper/dist/css/swiper.css' |
||||
|
|
||||
|
const defaultForm = { id: null, taskType: null, taskName: null, location: null, number: null, remark: null } |
||||
|
export default { |
||||
|
name: 'DataScreening', |
||||
|
components: { swiper, swiperSlide, crudOperation }, |
||||
|
cruds() { |
||||
|
return CRUD({ title: '架位总览', url: 'api/libraryRegion/initLibraryRegionList', crudMethod: { ...crudRegion }, sort: [], optShow: { |
||||
|
add: false, |
||||
|
edit: false, |
||||
|
del: false, |
||||
|
download: false, |
||||
|
group: false, |
||||
|
reset: false |
||||
|
}, |
||||
|
queryOnPresenterCreated: false |
||||
|
}) |
||||
|
}, |
||||
|
mixins: [presenter(), header(), form(defaultForm), crud()], |
||||
|
data() { |
||||
|
const _this = this |
||||
|
return { |
||||
|
floorName: null, |
||||
|
regionName: null, |
||||
|
bookShelfDetails: null, |
||||
|
booShelfGrid: null, |
||||
|
cellInfo: { |
||||
|
gridName: null, |
||||
|
startSortmark: null, |
||||
|
endSortmark: null, |
||||
|
cameraId: null |
||||
|
}, |
||||
|
callNumVisible: false, |
||||
|
layerNum: 0, |
||||
|
rackNum: 0, |
||||
|
swiperActiveIndex: 0, |
||||
|
cellIndex: null, |
||||
|
swiperOptionContent: { |
||||
|
slidesPerView: 'auto', |
||||
|
on: { |
||||
|
slideChangeTransitionStart: function() { |
||||
|
_this.cellIndex = null |
||||
|
_this.swiperActiveIndex = this.activeIndex |
||||
|
console.log('activeIndexffff', this.swiperActiveIndex) |
||||
|
_this.swiperTitle.slideTo(this.activeIndex, 500, false) |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
swiperOptionTitle: { |
||||
|
slidesPerView: 'auto', |
||||
|
freeMode: true |
||||
|
}, |
||||
|
layerVal: '001排', |
||||
|
layerOptions: [{ id: 1, name: '001排' }], |
||||
|
tabListData: [], |
||||
|
permission: { |
||||
|
add: ['admin', 'floor:add'], |
||||
|
edit: ['admin', 'floor:edit'], |
||||
|
del: ['admin', 'floor:del'] |
||||
|
}, |
||||
|
rules: { |
||||
|
taskName: [ |
||||
|
{ required: true, message: '请输入盘点单号', trigger: 'blur' } |
||||
|
], |
||||
|
taskType: [ |
||||
|
{ required: true, message: '请输入盘点类型', trigger: 'blur' } |
||||
|
], |
||||
|
location: [ |
||||
|
{ required: true, message: '请输入目标位置', trigger: 'blur' } |
||||
|
], |
||||
|
number: [ |
||||
|
{ required: true, message: '请输入目标数量', trigger: 'blur' } |
||||
|
] |
||||
|
}, |
||||
|
popoverIndex: 1, |
||||
|
popoverVisible: [], |
||||
|
popoverStyles: [] |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
...mapGetters([ |
||||
|
'user', |
||||
|
'baseApi' |
||||
|
]), |
||||
|
swiperContent() { |
||||
|
return this.$refs.swiperContent.$el.swiper |
||||
|
}, |
||||
|
swiperTitle() { |
||||
|
return this.$refs.swiperTitle.$el.swiper |
||||
|
}, |
||||
|
cellStyle: function() { |
||||
|
// const h = '100%/' + this.layerNum |
||||
|
// const w = '100%/' + this.rackNum |
||||
|
const h = '60px' |
||||
|
const w = '100%/' + this.rackNum |
||||
|
return { width: `calc(${w} - 4px )`, height: `calc(${h} - 2px)` } |
||||
|
} |
||||
|
}, |
||||
|
async created() { |
||||
|
if (localStorage.getItem('dataScreenRegion')) { |
||||
|
const dataScreenRegion = JSON.parse(localStorage.getItem('dataScreenRegion')) |
||||
|
this.floorName = dataScreenRegion.floorName |
||||
|
this.regionName = dataScreenRegion.regionName |
||||
|
// 单面/双面 |
||||
|
this.tabListData = dataScreenRegion.rowType === 1 |
||||
|
? dataScreenRegion.toward === 1 |
||||
|
? [{ name: 'A面' }] |
||||
|
: [{ name: 'B面' }] |
||||
|
: [{ name: 'A面' }, { name: 'B面' }] |
||||
|
|
||||
|
FetchBookShelfDetails({ 'shelfId': dataScreenRegion.id }).then(res => { |
||||
|
this.layerNum = res.shelfFloor |
||||
|
this.rackNum = res.shelfShelf |
||||
|
this.bookShelfDetails = res |
||||
|
this.getInitShelfGridByShelfId(this.bookShelfDetails.toward) |
||||
|
}).catch(() => { |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
handleToGrids() { |
||||
|
this.$router.push({ path: '/dataScreening/gird', query: { }}) |
||||
|
}, |
||||
|
[CRUD.HOOK.beforeRefresh]() { |
||||
|
}, |
||||
|
[CRUD.HOOK.afterRefresh](crud) { |
||||
|
}, |
||||
|
// 提交前的验证 |
||||
|
[CRUD.HOOK.afterValidateCU](crud) { |
||||
|
return true |
||||
|
}, |
||||
|
removeAreaPrefix(gridNames) { |
||||
|
const index = gridNames.indexOf('区') |
||||
|
if (index !== -1) { |
||||
|
return gridNames.substring(index + 1) |
||||
|
} |
||||
|
return gridNames |
||||
|
}, |
||||
|
getInitShelfGridByShelfId(toward) { |
||||
|
// rowType 1 单 2 双 |
||||
|
// toward 1 A面 2 B面 |
||||
|
// shelfType 1 '始终最左边为第1架(S型排架)' |
||||
|
// shelfType 2 'A面最左为第1架(B面最左为最后1架)' |
||||
|
// shelfType 3 'B面最左为第1架(A面最左为最后1架)' |
||||
|
// floorType 1 '最顶层为第一层(从上至下)' |
||||
|
// floorType 2 '最底层为第一层(从下至上)' |
||||
|
FetchInitShelfGridByShelfId({ 'shelfId': this.bookShelfDetails.id, 'toward': toward }).then(res => { |
||||
|
const sortFunction = toward === 1 ? { |
||||
|
1: { 1: 'sortBookshelvesLeftTop', 2: 'sortBookshelvesLeftBottom' }, |
||||
|
2: { 1: 'sortBookshelvesLeftTop', 2: 'sortBookshelvesLeftBottom' }, |
||||
|
3: { 1: 'sortBookshelvesRightTop', 2: 'sortBookshelvesRightBottom' } |
||||
|
} : { |
||||
|
1: { 1: 'sortBookshelvesLeftTop', 2: 'sortBookshelvesLeftBottom' }, |
||||
|
2: { 1: 'sortBookshelvesRightTop', 2: 'sortBookshelvesRightBottom' }, |
||||
|
3: { 1: 'sortBookshelvesLeftTop', 2: 'sortBookshelvesLeftBottom' } |
||||
|
} |
||||
|
const shelfType = this.bookShelfDetails.shelfType |
||||
|
const floorType = this.bookShelfDetails.floorType |
||||
|
const sortMethod = sortFunction[shelfType][floorType] |
||||
|
this.booShelfGrid = this[sortMethod](res) |
||||
|
|
||||
|
this.popoverVisible = Array(this.booShelfGrid.length).fill(false) |
||||
|
// this.popoverStyles = new Array(this.booShelfGrid.length).fill({ position: 'absolute', left: '20%', top: '48px' }) |
||||
|
}).catch(() => { |
||||
|
}) |
||||
|
}, |
||||
|
// 最左为第一架, 最顶层为第一层 从上往下 |
||||
|
sortBookshelvesLeftTop(data) { |
||||
|
const sortedData = [] |
||||
|
const maxFloor = Math.max(...data.map(item => parseInt(item.gridFloor))) |
||||
|
const maxShelf = Math.max(...data.map(item => parseInt(item.gridShelf.slice(-1)))) |
||||
|
|
||||
|
for (let i = 1; i <= maxFloor; i++) { |
||||
|
for (let j = 1; j <= maxShelf; j++) { |
||||
|
const currentShelf = data.find(item => parseInt(item.gridFloor) === i && parseInt(item.gridShelf.slice(-1)) === j) |
||||
|
if (currentShelf) { |
||||
|
sortedData.push(currentShelf) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return sortedData |
||||
|
}, |
||||
|
// 最右为第一架,最左为最后一架, 最顶层为第一层 从上往下 |
||||
|
sortBookshelvesRightTop(data) { |
||||
|
const sortedData = [] |
||||
|
// 获取最大的楼层数 |
||||
|
const maxFloor = Math.max(...data.map(item => parseInt(item.gridFloor))) |
||||
|
const maxShelf = Math.max(...data.map(item => parseInt(item.gridShelf.match(/\d+$/)[0]))) |
||||
|
for (let i = 1; i <= maxFloor; i++) { |
||||
|
// 从最大的书架层数开始,向下排序 |
||||
|
for (let j = maxShelf; j >= 1; j--) { |
||||
|
const currentShelf = data.find(item => parseInt(item.gridFloor) === i && parseInt(item.gridShelf.match(/\d+$/)[0]) === j) |
||||
|
if (currentShelf) { |
||||
|
sortedData.push(currentShelf) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return sortedData |
||||
|
}, |
||||
|
// 最左为第一架, 最底层为第一层 从下往上 |
||||
|
sortBookshelvesLeftBottom(data) { |
||||
|
const sortedData = [] |
||||
|
// 获取最大的楼层数 |
||||
|
const maxFloor = Math.max(...data.map(item => parseInt(item.gridFloor))) |
||||
|
// 获取最大的书架层数 |
||||
|
const maxShelf = Math.max(...data.map(item => parseInt(item.gridShelf.slice(-1)))) |
||||
|
for (let i = maxFloor; i >= 1; i--) { |
||||
|
for (let j = 1; j <= maxShelf; j++) { |
||||
|
const currentShelf = data.find(item => parseInt(item.gridFloor) === i && parseInt(item.gridShelf.slice(-1)) === j) |
||||
|
if (currentShelf) { |
||||
|
sortedData.push(currentShelf) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return sortedData |
||||
|
}, |
||||
|
// 最左为最后一架, 最底层为第一层 从下往上 |
||||
|
sortBookshelvesRightBottom(data) { |
||||
|
const sortedData = [] |
||||
|
// 获取最大的楼层数 |
||||
|
const maxFloor = Math.max(...data.map(item => parseInt(item.gridFloor))) |
||||
|
const maxShelfPerFloor = data.map(item => parseInt(item.gridShelf.match(/\d+$/)[0])) |
||||
|
.reduce((acc, curr, index, arr) => { |
||||
|
const floor = parseInt(data[index].gridFloor) |
||||
|
if (!acc[floor]) acc[floor] = 1 |
||||
|
if (acc[floor] < curr) acc[floor] = curr |
||||
|
return acc |
||||
|
}, {}) |
||||
|
// 从最大的楼层开始向下遍历 |
||||
|
for (let i = maxFloor; i >= 1; i--) { |
||||
|
// 从最大的书架编号开始向左遍历 |
||||
|
for (let j = maxShelfPerFloor[i] || 1; j >= 1; j--) { |
||||
|
const currentShelf = data.find(item => parseInt(item.gridFloor) === i && parseInt(item.gridShelf.match(/\d+$/)[0]) === j) |
||||
|
if (currentShelf) { |
||||
|
sortedData.push(currentShelf) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return sortedData |
||||
|
}, |
||||
|
handleSlidClickFun(index) { |
||||
|
this.cellIndex = null |
||||
|
this.handleSlideToFun(index) |
||||
|
if (localStorage.getItem('bookShelfDetails')) { |
||||
|
this.getInitShelfGridByShelfId(index + 1) |
||||
|
} |
||||
|
}, |
||||
|
handleSlideToFun(index) { |
||||
|
this.swiperActiveIndex = index |
||||
|
this.swiperContent.slideTo(index, 500, false) |
||||
|
this.swiperTitle.slideTo(index, 500, false) |
||||
|
}, |
||||
|
handleCellCurrent(item, index) { |
||||
|
console.log('index', index) |
||||
|
this.cellIndex = index |
||||
|
this.cellInfo = { |
||||
|
id: item.id, |
||||
|
gridName: item.gridName, |
||||
|
startSortmark: item.startSortmark, |
||||
|
endSortmark: item.endSortmark, |
||||
|
cameraId: item.cameraId, |
||||
|
check: item.isCheck, |
||||
|
order: item.isOrder |
||||
|
} |
||||
|
}, |
||||
|
showPopover(index) { |
||||
|
this.popoverIndex = index |
||||
|
// 显示对应的 popover |
||||
|
if (!this.popoverVisible[index]) { |
||||
|
this.$set(this.popoverVisible, index, true) |
||||
|
} |
||||
|
|
||||
|
const lastColumnIndexes = [] |
||||
|
const secondLastColumnIndexes = [] |
||||
|
|
||||
|
for (let i = 0; i < this.booShelfGrid.length; i++) { |
||||
|
// 计算当前数据项的列索引 |
||||
|
const columnIndex = i % this.rackNum |
||||
|
// 如果是最后一列(第5列) |
||||
|
if (columnIndex === this.rackNum - 1) { |
||||
|
lastColumnIndexes.push(i) |
||||
|
// 更新最后一列的样式 |
||||
|
this.$set(this.popoverStyles, i, { position: 'absolute', left: '-140px', top: '48px' }) |
||||
|
} |
||||
|
// 如果是倒数第二列(第4列) |
||||
|
if (columnIndex === this.rackNum - 2) { |
||||
|
secondLastColumnIndexes.push(i) |
||||
|
// 更新倒数第二列的样式 |
||||
|
this.$set(this.popoverStyles, i, { position: 'absolute', left: '-20px', top: '48px' }) |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
hidePopover() { |
||||
|
this.popoverIndex = null |
||||
|
// 隐藏所有的popover |
||||
|
this.popoverVisible.forEach((isVisible, index) => { |
||||
|
if (isVisible) { |
||||
|
this.$set(this.popoverVisible, index, false) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.container-right{ |
||||
|
min-height: calc(100vh - 232px) !important; |
||||
|
} |
||||
|
.venue-content{ |
||||
|
position: relative; |
||||
|
} |
||||
|
.venue-header{ |
||||
|
h4{ |
||||
|
flex: 1; |
||||
|
} |
||||
|
.bookshelf-area{ |
||||
|
padding-right: 30px; |
||||
|
font-weight: bold; |
||||
|
color: #0348F3; |
||||
|
} |
||||
|
} |
||||
|
.crud-opts{ |
||||
|
position: absolute; |
||||
|
right: 20px; |
||||
|
top: 10px; |
||||
|
} |
||||
|
.venue-left{ |
||||
|
flex: 1; |
||||
|
margin-right: 0 !important; |
||||
|
.venue-preview{ |
||||
|
height: 633px !important; |
||||
|
} |
||||
|
} |
||||
|
.venue-right{ |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
width: 400px; |
||||
|
padding: 50px 10px 20px 10px !important; |
||||
|
.lib-right-item{ |
||||
|
position: relative; |
||||
|
padding-bottom: 10px; |
||||
|
margin-bottom: 10px; |
||||
|
border: 1px solid #E8F2FF; |
||||
|
border-radius: 4px; |
||||
|
h4{ |
||||
|
padding: 6px 10px; |
||||
|
background-color: #E8F2FF; |
||||
|
color: #000; |
||||
|
line-height: 30px; |
||||
|
border-bottom: 1px solid #edeff3; |
||||
|
} |
||||
|
.refresh-date{ |
||||
|
position: absolute; |
||||
|
right: 14px; |
||||
|
top: 10px; |
||||
|
font-size: 12px; |
||||
|
line-height: 30px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.data-right-list { |
||||
|
padding-top: 10px; |
||||
|
li{ |
||||
|
display: flex; |
||||
|
justify-content: flex-start; |
||||
|
align-items: center; |
||||
|
line-height: 36px; |
||||
|
|
||||
|
p{ |
||||
|
width: 80px; |
||||
|
font-weight: bold; |
||||
|
text-align: right; |
||||
|
} |
||||
|
span{ |
||||
|
width: 140px; |
||||
|
display: block; |
||||
|
text-align: right; |
||||
|
i{ |
||||
|
font-style: normal; |
||||
|
font-weight: bold; |
||||
|
padding: 0 10px; |
||||
|
color: #0348f3; |
||||
|
} |
||||
|
&.percentage{ |
||||
|
width: auto; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.swiper-title{ |
||||
|
::v-deep .swiper-wrapper{ |
||||
|
margin: 10px 0; |
||||
|
border-bottom: 1px solid #EDEFF3; |
||||
|
} |
||||
|
} |
||||
|
.swiper-slide-title { |
||||
|
width: auto !important; |
||||
|
margin-right: 20px; |
||||
|
cursor: pointer; |
||||
|
.tab-name { |
||||
|
padding: 10px; |
||||
|
&.active { |
||||
|
color: #0348F3; |
||||
|
border-bottom: 3px solid #0348F3; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.swiper-content{ |
||||
|
height: 600px; |
||||
|
} |
||||
|
.tag-info{ |
||||
|
position: absolute; |
||||
|
right: 20px; |
||||
|
top: 34px; |
||||
|
display: flex; |
||||
|
justify-content: flex-start; |
||||
|
padding-left: 100px; |
||||
|
p{ |
||||
|
margin-left: 20px; |
||||
|
margin-top: -4px; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
} |
||||
|
.cabinet-row .cabinet-cell{ |
||||
|
overflow: inherit; |
||||
|
cursor: pointer; |
||||
|
span.cell-name{ |
||||
|
position: initial !important; |
||||
|
transform: none; |
||||
|
line-height: 58px; |
||||
|
} |
||||
|
} |
||||
|
::v-deep .cabinet-row .cabinet-cell span.el-popover__reference-wrapper{ |
||||
|
position: absolute !important; |
||||
|
left: 50% !important; |
||||
|
top: 20px !important; |
||||
|
transform: none; |
||||
|
width: 300px; |
||||
|
height: 210px; |
||||
|
background:rgba(0,0,0,.8); |
||||
|
color: #fff; |
||||
|
border-radius: 6px; |
||||
|
z-index: 99999999; |
||||
|
|
||||
|
.popover-content{ |
||||
|
.tooltip-top{ |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
height: 40px; |
||||
|
line-height: 40px; |
||||
|
padding: 0 10px; |
||||
|
border-bottom: 1px solid #fff; |
||||
|
} |
||||
|
.tooltip-top i{ |
||||
|
font-style: normal; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
ul{ |
||||
|
padding: 10px; |
||||
|
} |
||||
|
ul li{ |
||||
|
display: flex; |
||||
|
justify-content: flex-start; |
||||
|
align-items: center; |
||||
|
line-height: 36px; |
||||
|
font-style: normal; |
||||
|
} |
||||
|
|
||||
|
ul li p{ |
||||
|
width: 80px; |
||||
|
font-weight: bold; |
||||
|
text-align: right; |
||||
|
} |
||||
|
ul li em{ |
||||
|
width: 100px; |
||||
|
display: block; |
||||
|
text-align: right; |
||||
|
font-style: normal; |
||||
|
} |
||||
|
ul li i{ |
||||
|
font-style: normal; |
||||
|
font-weight: bold; |
||||
|
padding: 0 10px; |
||||
|
color: #0348f3; |
||||
|
} |
||||
|
ul li em.percentage{ |
||||
|
width: auto; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -1,159 +1,16 @@ |
|||||
<template> |
<template> |
||||
<div class="app-container row-container"> |
|
||||
<div class="head-container"> |
|
||||
<div class="head-search"> |
|
||||
<!-- 搜索 --> |
|
||||
<el-select v-model="query.type" clearable size="small" placeholder="类型" class="filter-item" style="width: 120px" @change="crud.toQuery"> |
|
||||
<i slot="prefix" class="iconfont icon-zhuangtai" /> |
|
||||
<el-option v-for="item in typeOptions" :key="item.key" :label="item.display_name" :value="item.key" /> |
|
||||
</el-select> |
|
||||
<el-select v-model="query.other" clearable size="small" placeholder="方式" class="filter-item" style="width: 120px" @change="crud.toQuery"> |
|
||||
<i slot="prefix" class="iconfont icon-zhuangtai" /> |
|
||||
<el-option v-for="item in otherOptions" :key="item.key" :label="item.display_name" :value="item.key" /> |
|
||||
</el-select> |
|
||||
<el-input v-model="query.search" clearable size="small" placeholder="输入关键字搜索" prefix-icon="el-icon-search" style="width: 220px;" class="filter-item" @clear="crud.toQuery" @keyup.enter.native="crud.toQuery" /> |
|
||||
<rrOperation /> |
|
||||
|
<div> |
||||
|
<router-view /> |
||||
</div> |
</div> |
||||
<crudOperation :permission="permission"> |
|
||||
<template v-slot:right> |
|
||||
<el-button :loading="crud.downloadLoading" size="mini" @click="doExport(crud.selections)"> |
|
||||
<i class="iconfont icon-daochu" /> |
|
||||
导出 |
|
||||
</el-button> |
|
||||
</template> |
</template> |
||||
</crudOperation> |
|
||||
</div> |
|
||||
<div class="container-wrap"> |
|
||||
<span class="right-top-line" /> |
|
||||
<span class="left-bottom-line" /> |
|
||||
<el-table |
|
||||
ref="table" |
|
||||
v-loading="crud.loading" |
|
||||
class="archives-table" |
|
||||
:data="crud.data" |
|
||||
style="width: 100%;" |
|
||||
height="590" |
|
||||
@selection-change="crud.selectionChangeHandler" |
|
||||
@row-click="clickRowHandler" |
|
||||
@row-dblclick="handleDbClick" |
|
||||
> |
|
||||
<el-table-column prop="regionName" label="类型" /> |
|
||||
<el-table-column prop="regionCode" label="方式" /> |
|
||||
<el-table-column prop="floorName" label="题名" /> |
|
||||
<el-table-column prop="booksheflCount" label="索书号" /> |
|
||||
<el-table-column prop="booksheflCount" label="条码" /> |
|
||||
<el-table-column prop="booksheflCount" label="目标层位" /> |
|
||||
<el-table-column prop="booksheflCount" label="盘点单号" /> |
|
||||
<el-table-column prop="booksheflCount" label="操作者" /> |
|
||||
<el-table-column prop="createTime" label="操作时间"> |
|
||||
<template slot-scope="scope"> |
|
||||
<div>{{ scope.row.createTime | parseTime }}</div> |
|
||||
</template> |
|
||||
</el-table-column> |
|
||||
</el-table> |
|
||||
<!--分页组件--> |
|
||||
<pagination v-if="crud.data.length!==0" /> |
|
||||
</div> |
|
||||
|
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
<script> |
||||
import crudRegion from '@/api/area/index' |
|
||||
import CRUD, { presenter, header, form, crud } from '@crud/crud' |
|
||||
import crudOperation from '@crud/CRUD.operation' |
|
||||
import rrOperation from '@crud/RR.operation' |
|
||||
import pagination from '@crud/Pagination' |
|
||||
import { mapGetters } from 'vuex' |
|
||||
// import { exportFile } from '@/utils/index' |
|
||||
// import qs from 'qs' |
|
||||
|
|
||||
const defaultForm = { id: null, taskType: null, taskName: null, location: null, number: null, remark: null } |
|
||||
export default { |
export default { |
||||
name: 'UpDownLog', |
|
||||
components: { crudOperation, rrOperation, pagination }, |
|
||||
cruds() { |
|
||||
return CRUD({ title: '上架/下架日志', url: 'api/libraryRegion/initLibraryRegionList', crudMethod: { ...crudRegion }, sort: [], optShow: { |
|
||||
add: false, |
|
||||
edit: false, |
|
||||
del: false, |
|
||||
download: false, |
|
||||
group: false, |
|
||||
reset: false |
|
||||
}}) |
|
||||
}, |
|
||||
mixins: [presenter(), header(), form(defaultForm), crud()], |
|
||||
|
name: 'CheckManage', |
||||
data() { |
data() { |
||||
return { |
return { |
||||
otherOptions: [ |
|
||||
{ key: '1', display_name: '自动' }, |
|
||||
{ key: '2', display_name: '手动' } |
|
||||
], |
|
||||
typeOptions: [ |
|
||||
{ key: '1', display_name: '上架' }, |
|
||||
{ key: '2', display_name: '下架' } |
|
||||
], |
|
||||
permission: { |
|
||||
add: ['admin', 'checkLog:add'], |
|
||||
edit: ['admin', 'checkLog:edit'], |
|
||||
del: ['admin', 'checkLog:del'] |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
computed: { |
|
||||
...mapGetters([ |
|
||||
'user', |
|
||||
'baseApi' |
|
||||
]) |
|
||||
}, |
|
||||
methods: { |
|
||||
[CRUD.HOOK.beforeRefresh]() { |
|
||||
}, |
|
||||
[CRUD.HOOK.afterRefresh](crud) { |
|
||||
}, |
|
||||
// 新增前将多选的值设置为空 |
|
||||
[CRUD.HOOK.beforeToAdd]() { |
|
||||
}, // 初始化编辑时候的角色与岗位 |
|
||||
[CRUD.HOOK.beforeToEdit](crud, form) { |
|
||||
}, |
|
||||
[CRUD.HOOK.beforeValidateCU](crud, form) { |
|
||||
}, |
|
||||
// 提交前做的操作 |
|
||||
[CRUD.HOOK.afterValidateCU](crud) { |
|
||||
return false |
|
||||
}, |
|
||||
clickRowHandler(row) { |
|
||||
this.$refs.table.clearSelection() |
|
||||
this.$refs.table.toggleRowSelection(row) |
|
||||
}, |
|
||||
doExport(data) { |
|
||||
console.log(data) |
|
||||
this.crud.downloadLoading = true |
|
||||
this.$confirm('此操作将导出所选数据' + '<span>你是否还要继续?</span>', '提示', { |
|
||||
confirmButtonText: '继续', |
|
||||
cancelButtonText: '取消', |
|
||||
type: 'warning', |
|
||||
dangerouslyUseHTMLString: true |
|
||||
}).then(() => { |
|
||||
const ids = [] |
|
||||
data.forEach(val => { |
|
||||
ids.push(val.deptsId) |
|
||||
}) |
|
||||
const params = { |
|
||||
'deptsIds': ids |
|
||||
} |
|
||||
console.log(params) |
|
||||
// exportFile(this.baseApi + '/api/depts/download?' + qs.stringify(params, { indices: false })) |
|
||||
// this.crud.downloadLoading = false |
|
||||
}).catch(() => { |
|
||||
}) |
|
||||
}, |
|
||||
handleCloseDialog() { |
|
||||
|
|
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
</script> |
</script> |
||||
|
|
||||
<style lang="scss" scoped> |
|
||||
|
<style scoped> |
||||
</style> |
</style> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue