2023年4月21日 添加功能

This commit is contained in:
bai 2023-04-21 00:41:02 +08:00
parent edeb5e8abd
commit fd33b72f79
23 changed files with 9309 additions and 2 deletions

View File

@ -14,10 +14,13 @@ VITE_DROP_CONSOLE = true
VITE_GLOB_API_URL=/jeecgboot VITE_GLOB_API_URL=/jeecgboot
#后台接口全路径地址(必填) #后台接口全路径地址(必填)
VITE_GLOB_DOMAIN_URL=http://localhost:8080/jeecg-boot VITE_GLOB_DOMAIN_URL=http://localhost:8090/jeecg-boot
# 接口前缀 # 接口前缀
VITE_GLOB_API_URL_PREFIX= VITE_GLOB_API_URL_PREFIX=
#微前端qiankun应用,命名必须以VITE_APP_SUB_开头,jeecg-app-1为子应用的项目名称,也是子应用的路由父路径 #微前端qiankun应用,命名必须以VITE_APP_SUB_开头,jeecg-app-1为子应用的项目名称,也是子应用的路由父路径
VITE_APP_SUB_jeecg-app-1 = '//localhost:8092' VITE_APP_SUB_jeecg-app-1 = '//localhost:8092'
#RTC服务器地址
VITE_GLOB_RTC_SERVER = http://GTR6-83T4P40:9001

View File

@ -21,6 +21,9 @@ VITE_GLOB_API_URL=/jeecg-boot
#后台接口全路径地址(必填) #后台接口全路径地址(必填)
VITE_GLOB_DOMAIN_URL=http://bylwcs.nenu.edu.cn/jeecg-boot VITE_GLOB_DOMAIN_URL=http://bylwcs.nenu.edu.cn/jeecg-boot
#RTC服务器地址
VITE_GLOB_RTC_SERVER = http://bylwcs.nenu.edu.cn/RTCServer
# 接口父路径前缀 # 接口父路径前缀
VITE_GLOB_API_URL_PREFIX= VITE_GLOB_API_URL_PREFIX=

3514
public/adapter.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

3514
public/resource/js/webrtc/adapter.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

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;
}

305
public/webrtcstreamer.js Normal file
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;
}

View File

@ -77,6 +77,14 @@ const site: AppRouteModule = {
title: '东北师范大学本科课堂教学质量评价表', title: '东北师范大学本科课堂教学质量评价表',
}, },
}, },
{
path: 'liveView',
name: 'LiveBroadcastRoom',
component: () => import('/@/views/site/common/webRTC/index.vue'),
meta: {
title: '直播',
}
}
], ],
}; };

View File

@ -0,0 +1,72 @@
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from "/@/hooks/web/useMessage";
const { createConfirm } = useMessage();
enum Api {
list = '/jiaoshi/kcJiaoshixinxibiao/list',
save='/jiaoshi/kcJiaoshixinxibiao/add',
edit='/jiaoshi/kcJiaoshixinxibiao/edit',
deleteOne = '/jiaoshi/kcJiaoshixinxibiao/delete',
deleteBatch = '/jiaoshi/kcJiaoshixinxibiao/deleteBatch',
importExcel = '/jiaoshi/kcJiaoshixinxibiao/importExcel',
exportXls = '/jiaoshi/kcJiaoshixinxibiao/exportXls',
}
/**
* api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* api
*/
export const getImportUrl = Api.importExcel;
/**
*
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
*
* @param params
* @param handleSuccess
*/
export const deleteOne = (params,handleSuccess) => {
return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
/**
*
* @param params
* @param handleSuccess
*/
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
});
}
/**
*
* @param params
* @param isUpdate
*/
export const saveOrUpdate = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params }, { isTransformResponse: false });
}

View File

@ -0,0 +1,172 @@
import {BasicColumn} from '/@/components/Table';
import {FormSchema} from '/@/components/Table';
import { rules} from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
//列表数据
export const columns: BasicColumn[] = [
{
title: '教室编号',
align: "center",
dataIndex: 'jsbh'
},
{
title: '教室名称',
align: "center",
dataIndex: 'jsmc'
},
{
title: '门牌号',
align: "center",
dataIndex: 'mph'
},
{
title: '校区',
align: "center",
dataIndex: 'xq'
},
{
title: '教室类型',
align: "center",
dataIndex: 'jslx'
},
{
title: '所属教学楼',
align: "center",
dataIndex: 'ssjxl'
},
{
title: '所属单位',
align: "center",
dataIndex: 'ssdw'
},
{
title: '排课优先级',
align: "center",
dataIndex: 'pkyxj'
},
{
title: '是否专用',
align: "center",
dataIndex: 'sfzy'
},
{
title: '教室类型2',
align: "center",
dataIndex: 'jslx2'
},
{
title: '座位数',
align: "center",
dataIndex: 'zws'
},
{
title: '考试座位数',
align: "center",
dataIndex: 'kszws'
},
{
title: '是否可用',
align: "center",
dataIndex: 'sfky'
},
{
title: '是否笔试考试',
align: "center",
dataIndex: 'sfbsks'
},
{
title: '是否可借用',
align: "center",
dataIndex: 'sfkjy'
},
];
//查询数据
export const searchFormSchema: FormSchema[] = [
];
//表单数据
export const formSchema: FormSchema[] = [
{
label: '教室编号',
field: 'jsbh',
component: 'Input',
},
{
label: '教室名称',
field: 'jsmc',
component: 'Input',
},
{
label: '门牌号',
field: 'mph',
component: 'Input',
},
{
label: '校区',
field: 'xq',
component: 'Input',
},
{
label: '教室类型',
field: 'jslx',
component: 'Input',
},
{
label: '所属教学楼',
field: 'ssjxl',
component: 'Input',
},
{
label: '所属单位',
field: 'ssdw',
component: 'Input',
},
{
label: '排课优先级',
field: 'pkyxj',
component: 'Input',
},
{
label: '是否专用',
field: 'sfzy',
component: 'Input',
},
{
label: '教室类型2',
field: 'jslx2',
component: 'Input',
},
{
label: '座位数',
field: 'zws',
component: 'Input',
},
{
label: '考试座位数',
field: 'kszws',
component: 'Input',
},
{
label: '是否可用',
field: 'sfky',
component: 'Input',
},
{
label: '是否笔试考试',
field: 'sfbsks',
component: 'Input',
},
{
label: '是否可借用',
field: 'sfkjy',
component: 'Input',
},
// TODO 主键隐藏字段目前写死为ID
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
];

View File

@ -0,0 +1,215 @@
<template>
<div>
<!--查询区域-->
<div class="jeecg-basic-table-form-container">
<a-form @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol">
<a-row :gutter="24">
</a-row>
</a-form>
</div>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button>批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/>
</template>
<!--字段回显插槽-->
<template #htmlSlot="{text}">
<div v-html="text"></div>
</template>
<!--省市区字段回显插槽-->
<!--<template #pcaSlot="{text}">
{{ getAreaTextByCode(text) }}
</template>-->
<template #fileSlot="{text}">
<span v-if="!text" style="font-size: 12px;font-style: italic;">无文件</span>
<a-button v-else :ghost="true" type="primary" preIcon="ant-design:download-outlined" size="small" @click="downloadFile(text)">下载</a-button>
</template>
</BasicTable>
<!-- 表单区域 -->
<KcJiaoshixinxibiaoModal ref="registerModal" @success="handleSuccess"></KcJiaoshixinxibiaoModal>
</div>
</template>
<script lang="ts" name="jiaoshi-kcJiaoshixinxibiao" setup>
import { ref, reactive } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { columns } from './KcJiaoshixinxibiao.data';
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './KcJiaoshixinxibiao.api';
import { downloadFile } from '/@/utils/common/renderUtils';
import KcJiaoshixinxibiaoModal from './components/KcJiaoshixinxibiaoModal.vue'
const queryParam = ref<any>({});
const toggleSearchStatus = ref<boolean>(false);
const registerModal = ref();
//table
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '教室信息表',
api: list,
columns,
canResize:false,
useSearchForm: false,
actionColumn: {
width: 120,
fixed: 'right',
},
beforeFetch: (params) => {
params.column = '',params.order = '';//
return Object.assign(params, queryParam.value);
},
},
exportConfig: {
name: "教室信息表",
url: getExportUrl,
},
importConfig: {
url: getImportUrl,
success: handleSuccess
},
});
const [registerTable, { reload, collapseAll, updateTableDataRecord, findTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] = tableContext;
const labelCol = reactive({
xs: { span: 24 },
sm: { span: 7 },
});
const wrapperCol = reactive({
xs: { span: 24 },
sm: { span: 16 },
});
/**
* 新增事件
*/
function handleAdd() {
registerModal.value.disableSubmit = false;
registerModal.value.add();
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
registerModal.value.disableSubmit = false;
registerModal.value.edit(record);
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
registerModal.value.disableSubmit = true;
registerModal.value.edit(record);
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
}, {
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
}
}
]
}
/**
* 查询
*/
function searchQuery() {
reload();
}
/**
* 重置
*/
function searchReset() {
queryParam.value = {};
selectedRowKeys.value = [];
//
reload();
}
</script>
<style lang="less" scoped>
.jeecg-basic-table-form-container {
.table-page-search-submitButtons {
display: block;
margin-bottom: 24px;
white-space: nowrap;
}
.query-group-cust{
width: calc(50% - 15px);
min-width: 100px !important;
}
.query-group-split-cust{
width: 30px;
display: inline-block;
text-align: center
}
}
</style>

View File

@ -0,0 +1,72 @@
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from "/@/hooks/web/useMessage";
const { createConfirm } = useMessage();
enum Api {
list = '/jiaoshi/kcZhihuijiaoshi/list',
save='/jiaoshi/kcZhihuijiaoshi/add',
edit='/jiaoshi/kcZhihuijiaoshi/edit',
deleteOne = '/jiaoshi/kcZhihuijiaoshi/delete',
deleteBatch = '/jiaoshi/kcZhihuijiaoshi/deleteBatch',
importExcel = '/jiaoshi/kcZhihuijiaoshi/importExcel',
exportXls = '/jiaoshi/kcZhihuijiaoshi/exportXls',
}
/**
* api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* api
*/
export const getImportUrl = Api.importExcel;
/**
*
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
*
* @param params
* @param handleSuccess
*/
export const deleteOne = (params,handleSuccess) => {
return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
/**
*
* @param params
* @param handleSuccess
*/
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
});
}
/**
*
* @param params
* @param isUpdate
*/
export const saveOrUpdate = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params }, { isTransformResponse: false });
}

View File

@ -0,0 +1,82 @@
import {BasicColumn} from '/@/components/Table';
import {FormSchema} from '/@/components/Table';
import { rules} from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
//列表数据
export const columns: BasicColumn[] = [
{
title: '校区',
align: "center",
dataIndex: 'xq'
},
{
title: '项目',
align: "center",
dataIndex: 'xm'
},
{
title: '新ip',
align: "center",
dataIndex: 'ip'
},
{
title: '账号',
align: "center",
dataIndex: 'user'
},
{
title: '密码',
align: "center",
dataIndex: 'mima'
},
{
title: '备注',
align: "center",
dataIndex: 'bz'
},
];
//查询数据
export const searchFormSchema: FormSchema[] = [
];
//表单数据
export const formSchema: FormSchema[] = [
{
label: '校区',
field: 'xq',
component: 'Input',
},
{
label: '项目',
field: 'xm',
component: 'Input',
},
{
label: '新ip',
field: 'ip',
component: 'Input',
},
{
label: '账号',
field: 'user',
component: 'Input',
},
{
label: '密码',
field: 'mima',
component: 'Input',
},
{
label: '备注',
field: 'bz',
component: 'Input',
},
// TODO 主键隐藏字段目前写死为ID
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
];

View File

@ -0,0 +1,215 @@
<template>
<div>
<!--查询区域-->
<div class="jeecg-basic-table-form-container">
<a-form @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol">
<a-row :gutter="24">
</a-row>
</a-form>
</div>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button>批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/>
</template>
<!--字段回显插槽-->
<template #htmlSlot="{text}">
<div v-html="text"></div>
</template>
<!--省市区字段回显插槽-->
<!--<template #pcaSlot="{text}">
{{ getAreaTextByCode(text) }}
</template>-->
<template #fileSlot="{text}">
<span v-if="!text" style="font-size: 12px;font-style: italic;">无文件</span>
<a-button v-else :ghost="true" type="primary" preIcon="ant-design:download-outlined" size="small" @click="downloadFile(text)">下载</a-button>
</template>
</BasicTable>
<!-- 表单区域 -->
<KcZhihuijiaoshiModal ref="registerModal" @success="handleSuccess"></KcZhihuijiaoshiModal>
</div>
</template>
<script lang="ts" name="jiaoshi-kcZhihuijiaoshi" setup>
import { ref, reactive } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { columns } from './KcZhihuijiaoshi.data';
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './KcZhihuijiaoshi.api';
import { downloadFile } from '/@/utils/common/renderUtils';
import KcZhihuijiaoshiModal from './components/KcZhihuijiaoshiModal.vue'
const queryParam = ref<any>({});
const toggleSearchStatus = ref<boolean>(false);
const registerModal = ref();
//table
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '智慧教室',
api: list,
columns,
canResize:false,
useSearchForm: false,
actionColumn: {
width: 120,
fixed: 'right',
},
beforeFetch: (params) => {
params.column = '',params.order = '';//
return Object.assign(params, queryParam.value);
},
},
exportConfig: {
name: "智慧教室",
url: getExportUrl,
},
importConfig: {
url: getImportUrl,
success: handleSuccess
},
});
const [registerTable, { reload, collapseAll, updateTableDataRecord, findTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] = tableContext;
const labelCol = reactive({
xs: { span: 24 },
sm: { span: 7 },
});
const wrapperCol = reactive({
xs: { span: 24 },
sm: { span: 16 },
});
/**
* 新增事件
*/
function handleAdd() {
registerModal.value.disableSubmit = false;
registerModal.value.add();
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
registerModal.value.disableSubmit = false;
registerModal.value.edit(record);
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
registerModal.value.disableSubmit = true;
registerModal.value.edit(record);
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
}, {
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
}
}
]
}
/**
* 查询
*/
function searchQuery() {
reload();
}
/**
* 重置
*/
function searchReset() {
queryParam.value = {};
selectedRowKeys.value = [];
//
reload();
}
</script>
<style lang="less" scoped>
.jeecg-basic-table-form-container {
.table-page-search-submitButtons {
display: block;
margin-bottom: 24px;
white-space: nowrap;
}
.query-group-cust{
width: calc(50% - 15px);
min-width: 100px !important;
}
.query-group-split-cust{
width: 30px;
display: inline-block;
text-align: center
}
}
</style>

View File

@ -0,0 +1,211 @@
<template>
<a-spin :spinning="confirmLoading">
<a-form ref="formRef" class="antd-modal-form" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-row>
<a-col :span="24">
<a-form-item label="教室编号" v-bind="validateInfos.jsbh">
<a-input v-model:value="formData.jsbh" placeholder="请输入教室编号" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="教室名称" v-bind="validateInfos.jsmc">
<a-input v-model:value="formData.jsmc" placeholder="请输入教室名称" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="门牌号" v-bind="validateInfos.mph">
<a-input v-model:value="formData.mph" placeholder="请输入门牌号" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="校区" v-bind="validateInfos.xq">
<a-input v-model:value="formData.xq" placeholder="请输入校区" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="教室类型" v-bind="validateInfos.jslx">
<a-input v-model:value="formData.jslx" placeholder="请输入教室类型" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="所属教学楼" v-bind="validateInfos.ssjxl">
<a-input v-model:value="formData.ssjxl" placeholder="请输入所属教学楼" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="所属单位" v-bind="validateInfos.ssdw">
<a-input v-model:value="formData.ssdw" placeholder="请输入所属单位" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="排课优先级" v-bind="validateInfos.pkyxj">
<a-input v-model:value="formData.pkyxj" placeholder="请输入排课优先级" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="是否专用" v-bind="validateInfos.sfzy">
<a-input v-model:value="formData.sfzy" placeholder="请输入是否专用" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="教室类型2" v-bind="validateInfos.jslx2">
<a-input v-model:value="formData.jslx2" placeholder="请输入教室类型2" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="座位数" v-bind="validateInfos.zws">
<a-input v-model:value="formData.zws" placeholder="请输入座位数" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="考试座位数" v-bind="validateInfos.kszws">
<a-input v-model:value="formData.kszws" placeholder="请输入考试座位数" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="是否可用" v-bind="validateInfos.sfky">
<a-input v-model:value="formData.sfky" placeholder="请输入是否可用" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="是否笔试考试" v-bind="validateInfos.sfbsks">
<a-input v-model:value="formData.sfbsks" placeholder="请输入是否笔试考试" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="是否可借用" v-bind="validateInfos.sfkjy">
<a-input v-model:value="formData.sfkjy" placeholder="请输入是否可借用" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-spin>
</template>
<script lang="ts" setup>
import { ref, reactive, defineExpose, nextTick, defineProps, computed, onMounted } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
import { getValueType } from '/@/utils';
import { saveOrUpdate } from '../KcJiaoshixinxibiao.api';
import { Form } from 'ant-design-vue';
const props = defineProps({
formDisabled: { type: Boolean, default: false },
formData: { type: Object, default: ()=>{} },
formBpm: { type: Boolean, default: true }
});
const formRef = ref();
const useForm = Form.useForm;
const emit = defineEmits(['register', 'ok']);
const formData = reactive<Record<string, any>>({
id: '',
jsbh: '',
jsmc: '',
mph: '',
xq: '',
jslx: '',
ssjxl: '',
ssdw: '',
pkyxj: '',
sfzy: '',
jslx2: '',
zws: '',
kszws: '',
sfky: '',
sfbsks: '',
sfkjy: '',
});
const { createMessage } = useMessage();
const labelCol = ref<any>({ xs: { span: 24 }, sm: { span: 5 } });
const wrapperCol = ref<any>({ xs: { span: 24 }, sm: { span: 16 } });
const confirmLoading = ref<boolean>(false);
//
const validatorRules = {
};
const { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: true });
//
const disabled = computed(()=>{
if(props.formBpm === true){
if(props.formData.disabled === false){
return false;
}else{
return true;
}
}
return props.formDisabled;
});
/**
* 新增
*/
function add() {
edit({});
}
/**
* 编辑
*/
function edit(record) {
nextTick(() => {
resetFields();
//
Object.assign(formData, record);
});
}
/**
* 提交数据
*/
async function submitForm() {
//
await validate();
confirmLoading.value = true;
const isUpdate = ref<boolean>(false);
//
let model = formData;
if (model.id) {
isUpdate.value = true;
}
//
for (let data in model) {
//
if (model[data] instanceof Array) {
let valueType = getValueType(formRef.value.getProps, data);
//
if (valueType === 'string') {
model[data] = model[data].join(',');
}
}
}
await saveOrUpdate(model, isUpdate.value)
.then((res) => {
if (res.success) {
createMessage.success(res.message);
emit('ok');
} else {
createMessage.warning(res.message);
}
})
.finally(() => {
confirmLoading.value = false;
});
}
defineExpose({
add,
edit,
submitForm,
});
</script>
<style lang="less" scoped>
.antd-modal-form {
min-height: 500px !important;
overflow-y: auto;
padding: 24px 24px 24px 24px;
}
</style>

View File

@ -0,0 +1,75 @@
<template>
<a-modal :title="title" :width="width" :visible="visible" @ok="handleOk" :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭">
<KcJiaoshixinxibiaoForm ref="registerForm" @ok="submitCallback" :formDisabled="disableSubmit" :formBpm="false"></KcJiaoshixinxibiaoForm>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, nextTick, defineExpose } from 'vue';
import KcJiaoshixinxibiaoForm from './KcJiaoshixinxibiaoForm.vue'
const title = ref<string>('');
const width = ref<number>(800);
const visible = ref<boolean>(false);
const disableSubmit = ref<boolean>(false);
const registerForm = ref();
const emit = defineEmits(['register', 'success']);
/**
* 新增
*/
function add() {
title.value = '新增';
visible.value = true;
nextTick(() => {
registerForm.value.add();
});
}
/**
* 编辑
* @param record
*/
function edit(record) {
title.value = disableSubmit.value ? '详情' : '编辑';
visible.value = true;
nextTick(() => {
registerForm.value.edit(record);
});
}
/**
* 确定按钮点击事件
*/
function handleOk() {
registerForm.value.submitForm();
}
/**
* form保存回调事件
*/
function submitCallback() {
handleCancel();
emit('success');
}
/**
* 取消按钮回调事件
*/
function handleCancel() {
visible.value = false;
}
defineExpose({
add,
edit,
disableSubmit,
});
</script>
<style>
/**隐藏样式-modal确定按钮 */
.jee-hidden {
display: none !important;
}
</style>

View File

@ -0,0 +1,157 @@
<template>
<a-spin :spinning="confirmLoading">
<a-form ref="formRef" class="antd-modal-form" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-row>
<a-col :span="24">
<a-form-item label="校区" v-bind="validateInfos.xq">
<a-input v-model:value="formData.xq" placeholder="请输入校区" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="项目" v-bind="validateInfos.xm">
<a-input v-model:value="formData.xm" placeholder="请输入项目" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="新ip" v-bind="validateInfos.ip">
<a-input v-model:value="formData.ip" placeholder="请输入新ip" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="账号" v-bind="validateInfos.user">
<a-input v-model:value="formData.user" placeholder="请输入账号" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="密码" v-bind="validateInfos.mima">
<a-input v-model:value="formData.mima" placeholder="请输入密码" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="备注" v-bind="validateInfos.bz">
<a-input v-model:value="formData.bz" placeholder="请输入备注" :disabled="disabled"></a-input>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-spin>
</template>
<script lang="ts" setup>
import { ref, reactive, defineExpose, nextTick, defineProps, computed, onMounted } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
import { getValueType } from '/@/utils';
import { saveOrUpdate } from '../KcZhihuijiaoshi.api';
import { Form } from 'ant-design-vue';
const props = defineProps({
formDisabled: { type: Boolean, default: false },
formData: { type: Object, default: ()=>{} },
formBpm: { type: Boolean, default: true }
});
const formRef = ref();
const useForm = Form.useForm;
const emit = defineEmits(['register', 'ok']);
const formData = reactive<Record<string, any>>({
id: '',
xq: '',
xm: '',
ip: '',
user: '',
mima: '',
bz: '',
});
const { createMessage } = useMessage();
const labelCol = ref<any>({ xs: { span: 24 }, sm: { span: 5 } });
const wrapperCol = ref<any>({ xs: { span: 24 }, sm: { span: 16 } });
const confirmLoading = ref<boolean>(false);
//
const validatorRules = {
};
const { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: true });
//
const disabled = computed(()=>{
if(props.formBpm === true){
if(props.formData.disabled === false){
return false;
}else{
return true;
}
}
return props.formDisabled;
});
/**
* 新增
*/
function add() {
edit({});
}
/**
* 编辑
*/
function edit(record) {
nextTick(() => {
resetFields();
//
Object.assign(formData, record);
});
}
/**
* 提交数据
*/
async function submitForm() {
//
await validate();
confirmLoading.value = true;
const isUpdate = ref<boolean>(false);
//
let model = formData;
if (model.id) {
isUpdate.value = true;
}
//
for (let data in model) {
//
if (model[data] instanceof Array) {
let valueType = getValueType(formRef.value.getProps, data);
//
if (valueType === 'string') {
model[data] = model[data].join(',');
}
}
}
await saveOrUpdate(model, isUpdate.value)
.then((res) => {
if (res.success) {
createMessage.success(res.message);
emit('ok');
} else {
createMessage.warning(res.message);
}
})
.finally(() => {
confirmLoading.value = false;
});
}
defineExpose({
add,
edit,
submitForm,
});
</script>
<style lang="less" scoped>
.antd-modal-form {
min-height: 500px !important;
overflow-y: auto;
padding: 24px 24px 24px 24px;
}
</style>

View File

@ -0,0 +1,75 @@
<template>
<a-modal :title="title" :width="width" :visible="visible" @ok="handleOk" :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭">
<KcZhihuijiaoshiForm ref="registerForm" @ok="submitCallback" :formDisabled="disableSubmit" :formBpm="false"></KcZhihuijiaoshiForm>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, nextTick, defineExpose } from 'vue';
import KcZhihuijiaoshiForm from './KcZhihuijiaoshiForm.vue'
const title = ref<string>('');
const width = ref<number>(800);
const visible = ref<boolean>(false);
const disableSubmit = ref<boolean>(false);
const registerForm = ref();
const emit = defineEmits(['register', 'success']);
/**
* 新增
*/
function add() {
title.value = '新增';
visible.value = true;
nextTick(() => {
registerForm.value.add();
});
}
/**
* 编辑
* @param record
*/
function edit(record) {
title.value = disableSubmit.value ? '详情' : '编辑';
visible.value = true;
nextTick(() => {
registerForm.value.edit(record);
});
}
/**
* 确定按钮点击事件
*/
function handleOk() {
registerForm.value.submitForm();
}
/**
* form保存回调事件
*/
function submitCallback() {
handleCancel();
emit('success');
}
/**
* 取消按钮回调事件
*/
function handleCancel() {
visible.value = false;
}
defineExpose({
add,
edit,
disableSubmit,
});
</script>
<style>
/**隐藏样式-modal确定按钮 */
.jee-hidden {
display: none !important;
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<div style="width:100%;height: 100%;">
<a-row>
<a-col :span="4">
<a-menu
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
style="overflow: hidden;overflow-y:scroll"
:style="{ height: `calc(100vh - ${topWidth})`, borderRight: 0 }"
mode="inline"
>
<a-menu-item v-for="(item, index) of leftList" :key="index" @click="(e) => titleClick(e,item)" :title="`${item.jsmc}-${item.xm}`">
{{ item.jsmc }}-{{ item.xm }}
</a-menu-item>
</a-menu>
<!-- document.querySelector('.ant-layout .jeecg-default-layout-main > div').style.height -->
</a-col>
<a-col :span="20">
<bVideo ref="bVideoMainRef" :videoOption="{ autoPlay: true }" :videoKey="'main'+currentItem.id"/>
</a-col>
</a-row>
</div>
<!-- <a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
:style="{ height: '100%', borderRight: 0 }"
mode="inline"
>
<a-menu-item v-for="(item, index) of leftList" :key="index" @click="(e) => titleClick(e,item)" :title="`${item.xq}-${item.jsmc}-${item.xm}`">
{{ item.xq }}-{{ item.jsmc }}-{{ item.xm }}
</a-menu-item>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-content> -->
<!-- {{ currentItem }} -->
<!-- <bVideo ref="bVideoMainRef" :videoOption="{ autoPlay: true }" :videoKey="'main'+currentItem.id"/> -->
<!-- <a-row>
<a-col :span="12">
<bVideo ref="bVideoMainRef" :videoOption="{ autoPlay: true }" :videoKey="'main'+currentItem.id"/>
</a-col>
<a-col :span="12">
<bVideo ref="bVideoLeftRef" :videoOption="{ autoPlay: true }" :videoKey="'main2'+currentItem.id"/>
</a-col>
</a-row> -->
<!-- </a-layout-content>
</a-layout> -->
<!-- </a-layout> -->
</template>
<script lang="ts" setup name="zhihuijiaoshiIndexPage">
import type { MenuProps } from 'ant-design-vue';
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from "/@/hooks/web/useMessage";
import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
import bVideo from '/@/views/site/common/webRTC/video.vue';
const bVideoMainRef = ref();
const _document = window.document;
// const bVideoLeftRef = ref();
const leftList = <any>ref([]);
const currentItem = <any>ref({});
const topWidth = <any>ref('0');
onMounted(() => {
list({ pageSize: -1 }).then(res => {
leftList.value = (res?.records) ?? [];
topWidth.value = _document?.querySelector('.ant-layout .jeecg-default-layout-main > div')?.style?.height?? '0';
// console.log(_document?.querySelector('.ant-layout .jeecg-default-layout-main > div')?.style?.height?? '0')
});
});
// const topWidthCalc = computed(() => {
// return _document?.querySelector('.ant-layout .jeecg-default-layout-main > div')?.style?.height?? '0';
// });
// xxx = document.querySelector('.ant-layout .jeecg-default-layout-main > div').style.height
//
enum Api {
list = '/jiaoshi/kcZhihuijiaoshi/list',
}
/**
* 列表接口
* @param params
*/
const list = (params) => defHttp.get({ url: Api.list, params });
const selectedKeys = ref<string[]>(['1']);
const openKeys = ref<string[]>(['sub1']);
const titleClick = (e: Event, item: any) => {
console.log('titleClick ->', e, item);
currentItem.value = item;
playerVideo(item);
};
function playerVideo(item){
bVideoMainRef.value.connectFn(item.ip);
// bVideoMainRef.value.connectFn('rtsp://176.139.87.16/axis-media/media.amp','external');
// bVideoLeftRef.value.connectFn('rtsp://176.139.87.16/axis-media/media.amp','external');
}
//webRtc
onBeforeUnmount(() => {
bVideoMainRef.value.closeWebRtcStreamerFn();
})
</script>
<style lang="less" scoped>
</style>

View File

@ -17,6 +17,7 @@
</div> </div>
<span class="topTitle"> <span class="topTitle">
{{ projectName }} {{ projectName }}
<RouterLink hidden target='_blank' :to="{path:'/site/liveView',query:{ url: 'rtsp://176.139.87.16/axis-media/media.amp' }}">直播测试页</RouterLink>
</span> </span>
<span class="topRight"> <span class="topRight">
<a-dropdown> <a-dropdown>

View File

@ -0,0 +1,38 @@
<template>
<!-- <div @click="onPlay">点击开始播放(连接)直播</div>
<div @click="onPlay2">点击开始播放(连接)直播2</div> -->
<!-- <div>{{ route.query.url }}</div> -->
<bVideo ref="bVideoRef"/>
<!-- <video id="video" muted="" playsinline="" controls="" style="opacity: 1;"></video> -->
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount} from 'vue';
import { useRoute } from 'vue-router'
import bVideo from '/@/views/site/common/webRTC/video.vue';
const bVideoRef = ref();
const route = useRoute();
//webRtc
onBeforeUnmount(() => {
bVideoRef.value.closeWebRtcStreamerFn();
})
onMounted(() => {
if(route.query.url){
bVideoRef.value.connectFn(route.query.url);
}
})
</script>
<style lang="less" scoped>
video {
//margin: auto;
//left: 0;
//right: 0;
//position: relative;
background: grey;
}
</style>

View File

@ -0,0 +1,40 @@
// import { ref, nextTick, onBeforeUnmount} from 'vue';
export const rtcServerUrl = import.meta.env.VITE_GLOB_RTC_SERVER;
export function initWebRtcStreamer(domId) {
const windowObj = <any>window;
const WebRtcStreamer = windowObj.WebRtcStreamer;
console.log('初始化连机器!',domId);
if(WebRtcStreamer){
//获取webRtc服务
//video需要绑定的video控件ID
//127.0.0.1:8000启动webrtc-streamer的设备IP和端口默认8000
// webRtcServer.value = new WebRtcStreamer('video', location.protocol + '//192.168.10.26:8000')
const webRtcServer = new WebRtcStreamer(domId, rtcServerUrl);
//需要查看的rtsp地址
// webRtcServer.connect(`rtsp://176.139.87.16/axis-media/media.amp`)
//windowObj.__webRtcServer = webRtcServer
return webRtcServer;
}
return null;
}
export function connect(webRtcServer,videoUrl) {
console.log('连接!',webRtcServer,videoUrl);
if(webRtcServer && videoUrl){
console.log('连接2222',webRtcServer,videoUrl);
webRtcServer.connect(videoUrl);
}
}
export function closeWebRtcStreamer(webRtcServer) {
console.log('注销!',webRtcServer);
if(webRtcServer){
webRtcServer.disconnect()
// webRtcServer = null
}
}

View File

@ -0,0 +1,115 @@
<template>
<!-- <div @click="onPlay">点击开始播放(连接)直播</div>
<div @click="onPlay2">点击开始播放(连接)直播2</div> -->
<!-- <div>{{ route.query.url }}</div> -->
<!-- <span>播放地址:{{ path }}</span> -->
<video :id="props.videoKey" v-bind="videoOption"></video>
<!-- <video id="video" muted="" playsinline="" controls="" style="opacity: 1;"></video> -->
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted, nextTick, onBeforeUnmount} from 'vue';
import { propTypes } from '/@/utils/propTypes';
import { initWebRtcStreamer, connect, closeWebRtcStreamer } from '/@/views/site/common/webRTC/util';
const props = defineProps({
videoOption: propTypes.object.def({}),
videoKey: propTypes.string.def('video'),
});
const videoOption = reactive(Object.assign({
// width: '800', //
// height: '450', //
color: "#409eff", //
title: '', //
//src: "https://go.dreamwq.com/videos/IronMan.mp4", //
muted: true, //
playsinline: true,
webFullScreen: false,
speedRate: ["0.75", "1.0", "1.25", "1.5", "2.0"], //
autoPlay: false, //
loop: false, //
mirror: false, //
ligthOff: false, //
volume: 0.3, //
controls: true, //
},props.videoOption));
let webRtcServer = ref(null);
const path = ref('');
//webRtc
nextTick(() => {
//webRtcServer = initWebRtcStreamer('video');
});
onMounted(() => {
webRtcServer.value = initWebRtcStreamer(props.videoKey);
})
function getVideo(){
return document.querySelector('#'+props.videoKey)
}
function initWebRtcStreamerFn(){
webRtcServer.value = initWebRtcStreamer(props.videoKey);
return webRtcServer.value;
}
function closeWebRtcStreamerFn(){
closeWebRtcStreamer(webRtcServer.value);
webRtcServer.value = null
}
function connectFn(url:string,type?){
let _path = `rtsp://${url}/stream/0?config.login=web`;
if(type == 'external'){
_path = url;
}
path.value = _path;
connect(webRtcServer.value,path.value);
return webRtcServer.value;
}
//webRtc
onBeforeUnmount(() => {
closeWebRtcStreamerFn()
})
defineExpose({
webRtcServer,
getVideo,
initWebRtcStreamerFn,
closeWebRtcStreamerFn,
connectFn,
});
// function onPlay(){
// closeWebRtcStreamer(webRtcServer);
// webRtcServer = initWebRtcStreamer('video');
// if(webRtcServer){
// connect(webRtcServer,'rtsp://176.139.87.16/axis-media/media.amp');
// }
// }
// function onPlay2(){
// closeWebRtcStreamer(webRtcServer);
// webRtcServer = initWebRtcStreamer('video');
// if(webRtcServer){
// connect(webRtcServer,'rtsp://77.110.228.219/axis-media/media.amp');
// }
// }
</script>
<style lang="less" scoped>
video {
width: 100%;
//height: 100%;
//margin: auto;
//left: 0;
//right: 0;
//position: relative;
background: grey;
}
</style>

View File

@ -130,7 +130,7 @@ const { createMessage } = useMessage();
const route = useRoute(); const route = useRoute();
onMounted(() => { onMounted(() => {
console.log(route.query ); console.log( route.query );
let params = { pageSize: -1, status: 0, column: 'ordernum', order: 'asc' } let params = { pageSize: -1, status: 0, column: 'ordernum', order: 'asc' }
let queryQue = defHttp.get({ url: Api.que, params: { ...params, evaluationver: route.query.type } }); let queryQue = defHttp.get({ url: Api.que, params: { ...params, evaluationver: route.query.type } });
let queryAns = defHttp.get({ url: Api.ans, params }); let queryAns = defHttp.get({ url: Api.ans, params });