修改图片,视频查看等功能

玉溪
LAPTOP-S9HJSOEB\昊天 1 year ago
parent 7de35609a7
commit 2587f52757

@ -1,5 +1,5 @@
# 标题
VITE_APP_TITLE=随行管理系统
VITE_APP_TITLE=智慧盘点系统
# 项目本地运行端口号
VITE_PORT=80

@ -4,7 +4,7 @@ NODE_ENV=production
VITE_DEV=true
# 请求路径
VITE_BASE_URL='http://127.0.0.1:48080'
VITE_BASE_URL='http://192.168.0.10:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server

@ -4,7 +4,7 @@ NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://127.0.0.1:48080'
VITE_BASE_URL='http://192.168.0.10:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server

@ -4,7 +4,7 @@ NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://127.0.0.1:48080'
VITE_BASE_URL='http://192.168.0.10:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server

@ -4,7 +4,7 @@ NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://127.0.0.1:48080'
VITE_BASE_URL='http://192.168.0.10:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server

14
package-lock.json generated

@ -33,6 +33,7 @@
"element-plus": "^2.6.1",
"fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0",
"js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2",
"lodash-es": "^4.17.21",
"min-dash": "^4.1.1",
@ -10541,6 +10542,14 @@
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
"dev": true
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -24216,6 +24225,11 @@
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
"dev": true
},
"js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

Binary file not shown.

@ -49,6 +49,7 @@
"element-plus": "^2.6.1",
"fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0",
"js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2",
"lodash-es": "^4.17.21",
"min-dash": "^4.1.1",

@ -70,31 +70,31 @@ export const CameraApi = {
export const CameraIoApi = {
//查询相机io列表
getCameraIoList:async () => {
return request.get({ url: `/camera/camera-io/list` })
getCameraIoList: async (data): Promise<CameraIOVO[]> => {
return request.post({ url: `/logistics/camera-io/list`,data })
},
// 新增相机io
createCameraIo: async (data: CameraVO) => {
return await request.post({ url: `/camera/camera-io/create`, data })
createCameraIo: async (data: CameraIOVO) => {
return await request.post({ url: `/logistics/camera-io/create`, data })
},
// 修改相机io
updateCameraIo: async (data: CameraVO) => {
return await request.put({ url: `/camera/camera-io/update`, data })
return await request.put({ url: `/logistics/camera-io/update`, data })
},
// 删除相机io
deleteCameraIo: async (id: number) => {
return await request.delete({ url: `/camera/camera-io/delete?id=` + id })
return await request.delete({ url: `/logistics/camera-io/delete?id=` + id })
},
// 调用预置点位
callPtz: async (data: CameraIOVO) => {
return await request.post({ url: `/camera/camera-io/callPtz`, data })
return await request.post({ url: `/logistics/camera-io/callPtz`, data })
},
// 覆盖预置点位
overwritePtz: async (data: CameraIOVO) => {
return await request.post({ url: `/camera/camera-io/overwritePtz`, data })
return await request.post({ url: `/logistics/camera-io/overwritePtz`, data })
},
}

@ -10,6 +10,12 @@ export const CheckLogApi = {
getCheckLogPage: async (params: any) => {
return await request.get({ url: `/logistics/check-log/page`, params })
},
// 查询盘点分页
getColumns: async () => {
return await request.post({ url: `/logistics/check-log/getColumns` })
},
// 查询盘点详情
getCheckLog: async (id: number) => {

@ -50,10 +50,17 @@ export const StockApi = {
return await request.post({ url: `/logistics/stock/create`, data })
},
// 获得巷道所有盘点状态
getStreetStatus: async (data) => {
return await request.post({ url: `/logistics/stock/getStreetStatus`, data })
},
// 新增盘点状态
getStreetList: async (data) => {
return await request.get({ url: `/logistics/stock/getStreetList?streetId=`+ data.streetId+"&direction="+data.direction })
return await request.get({
url:
`/logistics/stock/getStreetList?streetId=` + data.streetId + '&direction=' + data.direction
})
},
// 修改盘点状态
updateStock: async (data: StockVO) => {
@ -69,4 +76,9 @@ export const StockApi = {
exportStock: async (params) => {
return await request.download({ url: `/logistics/stock/export-excel`, params })
},
//请求卡片信息接口
getStockStatus: async (data: any) => {
return await request.post({ url: `/logistics/stock/getStockStatus`, data })
}
}

@ -1,218 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<script src="https://unpkg.com/vue@next"></script>
<!-- <script src="./webrtc-vue/vue.global.prod.js" type="text/javascript" charset="utf-8"></script> -->
<script src="./ZLMRTCClient.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
<div id="app">
<div style="text-align: center">
<div>
<video ref="video" controls autoplay style="text-align: left">
Your browser is too old which doesn't support HTML5 video.
</video>
<video ref="selfVideo" controls autoplay style="text-align: right">
Your browser is too old which doesn't support HTML5 video.
</video>
</div>
</div>
</div>
</body>
<script>
const App = {
data() {
return {
streamUrl: document.location.protocol + "//" + window.location.host +
"/index/api/webrtc?app=live&stream=test&type=play",
player: null,
recvOnly: true,
resolutions: [],
resolution: '',
}
},
mounted() {
let ishttps = "https:" == document.location.protocol ? true : false;
let isLocal = "file:" == document.location.protocol ? true : false;
if (!ishttps && !isLocal) {
alert(
"本demo需要在https的网站访问 ,如果你要推流的话(this demo must access in site of https if you want push stream)"
);
}
if (isLocal) {
streamUrl.value =
"http://127.0.0.1" + "/index/api/webrtc?app=live&stream=test&type=play";
}
ZLMRTCClient.GetAllScanResolution().forEach((r, i) => {
let text = r.label + "(" + r.width + "x" + r.height + ")";
this.resolutions.push({
text: text,
value: r
});
});
this.resolution = this.resolutions[0].text;
},
methods: {
//点击radio事件
radioChange(value) {
let url = new URL(this.streamUrl);
console.log(value);
url.searchParams.set("type", value);
this.streamUrl = url;
if (value == "play") {
this.recvOnly = true;
} else if (value == "echo") {
this.recvOnly = false;
} else {
this.recvOnly = false;
}
},
changeResolution(e) {
this.resolution = e.target.options[e.target.selectedIndex].text;
},
start_play() {
let res = this.resolution.match(/\d+/g);
let h = parseInt(res.pop());
let w = parseInt(res.pop());
this.player = new ZLMRTCClient.Endpoint({
element: this.$refs.video, // video 标签
debug: true, // 是否打印日志
zlmsdpUrl: this.streamUrl, //流地址
simulcast: this.$refs.simulcast.checked,
useCamera: this.$refs.useCamera.checked,
audioEnable: this.$refs.audioEnable.checked,
videoEnable: this.$refs.videoEnable.checked,
recvOnly: this.recvOnly,
resolution: {
w: w,
h: h
},
usedatachannel: this.$refs.datachannel.checked,
});
this.player.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, function(e) {
// ICE 协商出错
console.log("ICE 协商出错");
});
this.player.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, function(e) {
//获取到了远端流,可以播放
console.log("播放成功", e.streams);
});
this.player.on(
ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED,
function(e) {
// offer anwser 交换失败
console.log("offer anwser 交换失败", e);
stop();
}
);
this.player.on(ZLMRTCClient.Events.WEBRTC_ON_LOCAL_STREAM, function(s) {
// 获取到了本地流
this.$refs.selfVideo.srcObject = s;
this.$refs.selfVideo.muted = true;
//console.log('offer anwser 交换失败',e)
});
this.player.on(ZLMRTCClient.Events.CAPTURE_STREAM_FAILED, function(s) {
// 获取本地流失败
console.log("获取本地流失败");
});
this.player.on(
ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE,
function(state) {
// RTC 状态变化 ,详情参考 https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState
console.log("当前状态==>", state);
}
);
this.player.on(
ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_OPEN,
function(event) {
console.log("rtc datachannel 打开 :", event);
}
);
this.player.on(
ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_MSG,
function(event) {
console.log("rtc datachannel 消息 :", event.data);
this.$refs.msgrecv.value = event.data;
}
);
this.player.on(
ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_ERR,
function(event) {
console.log("rtc datachannel 错误 :", event);
}
);
this.player.on(
ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_CLOSE,
function(event) {
console.log("rtc datachannel 关闭 :", event);
}
);
},
startVideo() {
console.log("开始");
this.stopVideo();
let res = this.resolution.match(/\d+/g);
let h = parseInt(res.pop());
let w = parseInt(res.pop());
if (this.$refs.useCamera.checked && !this.recvOnly) {
ZLMRTCClient.isSupportResolution(w, h)
.then((e) => {
this.start_play();
})
.catch((e) => {
alert("not support resolution");
});
} else {
this.start_play();
}
},
stopVideo() {
console.log("停止");
if (this.player) {
this.player.close();
this.player = null;
var remote = this.$refs.video;
if (remote) {
remote.srcObject = null;
remote.load();
}
var local = this.$refs.selfVideo;
local.srcObject = null;
local.load();
}
},
send() {
if (this.player) {
//send msg refernece https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/send
this.player.sendMsg(this.$refs.msgsend.value);
}
},
close() {
if (this.player) {
this.player.closeDataChannel();
}
}
}
};
Vue.createApp(App).mount('#app');
</script>
</html>

@ -1,269 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<script src="https://unpkg.com/vue@next"></script>
<!-- <script src="./webrtc-vue/vue.global.prod.js" type="text/javascript" charset="utf-8"></script> -->
<script src="./ZLMRTCClient.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
<div id="app">
<div style="text-align: center">
<div>
<video ref="video" controls autoplay style="text-align: left">
Your browser is too old which doesn't support HTML5 video.
</video>
<video ref="selfVideo" controls autoplay style="text-align: right">
Your browser is too old which doesn't support HTML5 video.
</video>
</div>
<div>
<p>
<label for="streamUrl">url:</label>
<input type="text" style="co" ref="url" v-model="streamUrl" />
</p>
<p>
<label for="simulcast">simulcast:</label>
<input type="checkbox" ref="simulcast" />
</p>
<p>
<label for="useCamera">useCamera:</label>
<input type="checkbox" checked="checked" ref="useCamera" />
</p>
<p>
<label for="audioEnable">audioEnable:</label>
<input type="checkbox" ref="audioEnable" checked="checked" />
</p>
<p>
<label for="videoEnable">videoEnable:</label>
<input type="checkbox" ref="videoEnable" checked="checked" />
</p>
<p>
<label for="method">method(play or push or echo):</label>
<input type="radio" name="method" @click="radioChange('echo')" value="echo" />echo
<input type="radio" name="method" @click="radioChange('push')" value="push" />push
<input type="radio" name="method" @click="radioChange('play')" value="play"
checked="true" />play
</p>
<p>
<label for="resolution">resolution:</label>
<!-- <select id="resolution" :value="resolution"></select> -->
<select @change="changeResolution">
<option :value="resolution.value" v-for="resolution in resolutions">
{{ resolution.text }}
</option>
</select>
</p>
<p>
<label for="datachannel">datachannel:</label>
<input ref="datachannel" name="datachannel" type="checkbox" value="0" />
</p>
<button @click="startVideo">开始(start)</button>
<button @click="stopVideo">停止(stop)</button>
<p>
<label for="msgsend">msgsend:</label>
<input type="text" ref="msgsend" value="hello word !" />
</p>
<p>
<label for="msgrecv">msgrecv:</label>
<input type="text" ref="msgrecv" disabled />
</p>
<button @click="send">发送(send by datachannel)</button>
<button @click="close">关闭(close datachannel)</button>
</div>
</div>
</div>
</body>
<script>
const App = {
data() {
return {
streamUrl: document.location.protocol + "//" + window.location.host +
"/index/api/webrtc?app=live&stream=test&type=play",
player: null,
recvOnly: true,
resolutions: [],
resolution: '',
}
},
mounted() {
if (isLocal) {
streamUrl.value =
"http://127.0.0.1" + "/index/api/webrtc?app=live&stream=test&type=play";
}
ZLMRTCClient.GetAllScanResolution().forEach((r, i) => {
let text = r.label + "(" + r.width + "x" + r.height + ")";
this.resolutions.push({
text: text,
value: r
});
});
this.resolution = this.resolutions[0].text;
},
methods: {
//点击radio事件
radioChange(value) {
let url = new URL(this.streamUrl);
console.log(value);
url.searchParams.set("type", value);
this.streamUrl = url;
if (value == "play") {
this.recvOnly = true;
} else if (value == "echo") {
this.recvOnly = false;
} else {
this.recvOnly = false;
}
},
changeResolution(e) {
this.resolution = e.target.options[e.target.selectedIndex].text;
},
start_play() {
let res = this.resolution.match(/\d+/g);
let h = parseInt(res.pop());
let w = parseInt(res.pop());
this.player = new ZLMRTCClient.Endpoint({
element: this.$refs.video, // video 标签
debug: true, // 是否打印日志
zlmsdpUrl: this.streamUrl, //流地址
simulcast: this.$refs.simulcast.checked,
useCamera: this.$refs.useCamera.checked,
audioEnable: this.$refs.audioEnable.checked,
videoEnable: this.$refs.videoEnable.checked,
recvOnly: this.recvOnly,
resolution: {
w: w,
h: h
},
usedatachannel: this.$refs.datachannel.checked,
});
this.player.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, function(e) {
// ICE 协商出错
console.log("ICE 协商出错");
});
this.player.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, function(e) {
//获取到了远端流,可以播放
console.log("播放成功", e.streams);
});
this.player.on(
ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED,
function(e) {
// offer anwser 交换失败
console.log("offer anwser 交换失败", e);
stop();
}
);
this.player.on(ZLMRTCClient.Events.WEBRTC_ON_LOCAL_STREAM, function(s) {
// 获取到了本地流
this.$refs.selfVideo.srcObject = s;
this.$refs.selfVideo.muted = true;
//console.log('offer anwser 交换失败',e)
});
this.player.on(ZLMRTCClient.Events.CAPTURE_STREAM_FAILED, function(s) {
// 获取本地流失败
console.log("获取本地流失败");
});
this.player.on(
ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE,
function(state) {
// RTC 状态变化 ,详情参考 https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState
console.log("当前状态==>", state);
}
);
this.player.on(
ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_OPEN,
function(event) {
console.log("rtc datachannel 打开 :", event);
}
);
this.player.on(
ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_MSG,
function(event) {
console.log("rtc datachannel 消息 :", event.data);
this.$refs.msgrecv.value = event.data;
}
);
this.player.on(
ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_ERR,
function(event) {
console.log("rtc datachannel 错误 :", event);
}
);
this.player.on(
ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_CLOSE,
function(event) {
console.log("rtc datachannel 关闭 :", event);
}
);
},
startVideo() {
console.log("开始");
this.stopVideo();
let res = this.resolution.match(/\d+/g);
let h = parseInt(res.pop());
let w = parseInt(res.pop());
if (this.$refs.useCamera.checked && !this.recvOnly) {
ZLMRTCClient.isSupportResolution(w, h)
.then((e) => {
this.start_play();
})
.catch((e) => {
alert("not support resolution");
});
} else {
this.start_play();
}
},
stopVideo() {
console.log("停止");
if (this.player) {
this.player.close();
this.player = null;
var remote = this.$refs.video;
if (remote) {
remote.srcObject = null;
remote.load();
}
var local = this.$refs.selfVideo;
local.srcObject = null;
local.load();
}
},
send() {
if (this.player) {
//send msg refernece https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/send
this.player.sendMsg(this.$refs.msgsend.value);
}
},
close() {
if (this.player) {
this.player.closeDataChannel();
}
}
}
};
Vue.createApp(App).mount('#app');
</script>
</html>

@ -4,61 +4,47 @@
<el-col :span="12">
<Camera :cameraId="cameraId" :ptzShow="true" :key="cameraId" />
</el-col>
<el-col :span="12">
<el-table
:data="configData"
:row-key="record => record.id"
:data="list"
:row-key="(record) => record.id"
@selection-change="getConfigIoList"
:pagination="false"
style="width: 100%; height: 335px; overflow-y: auto;"
style="width: 100%; height: 335px; overflow-y: auto"
max-height="800"
>
<el-table-column
prop="name"
label="指令名称"
width="320"
>
<el-table-column prop="name" label="指令名称">
<template #default="scope">
<el-button
type="text"
@click="toPtz(scope.row)"
>
<el-button type="text" @click="toPtz(scope.row)">
{{ scope.row.name }}({{ scope.row.code }})
</el-button>
</template>
</el-table-column>
<el-table-column
prop="position"
label="配置"
width="180"
>
<el-table-column prop="position" label="配置">
<template #default="scope">
<el-button
v-if="scope.row.position"
type="text"
class="plc"
@click="coverage(0, scope.row)"
@click="overwritePtz(scope.row.ptzId)"
>
覆盖
</el-button>
<el-button
v-else
type="text"
class="plc"
@click="coverage(0, scope.row)"
>
写入
</el-button>
<el-button
type="text"
class="plc"
@click="showModelDel(scope.row.id, scope.row)"
>
<el-button type="text" class="plc" @click="showModelDel(scope.row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-space wrap>
指令编码
<el-input v-model="formData.code" style="width: 80px" placeholder="编码" />
指令名称
<el-input v-model="formData.name" style="width: 80px" placeholder="名称" />
<el-button @click="addRouters"></el-button>
</el-space>
</el-col>
</el-row>
</ContentWrap>
@ -67,44 +53,82 @@
<script setup lang="ts">
import Camera from '@/components/camera/camera.vue'
import { CameraApi, CameraVO, CameraIoApi, CameraIOVO } from '@/api/logistics/camera'
/** 相机 列表 */
defineOptions({ name: 'Camera' })
import { ref, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
const message = useMessage() //
const { t } = useI18n() //
// props
const props = defineProps({
cameraId: {
type: Number,
default: 2
},
ptzShow: {
type: Boolean,
default: true
}
const props = defineProps<{
cameraId: number
ptzShow?: boolean
}>()
const formData = ref<{
id?: number
cameraId?: number
type?: string
ptzId?: number
name?: string
code?: string
}>({
id: undefined,
cameraId: props.cameraId,
type: undefined,
ptzId: undefined,
name: undefined,
code: undefined
})
const list = ref<CameraIOVO[]>([]) //
//
//
const getList = async () => {
console.log(props.cameraId);
try {
const data = await CameraIoApi.getCameraIoList() // Promise
const data = await CameraIoApi.getCameraIoList(formData.value) // cameraId undefined
list.value = data || [] //
} catch (error) {
console.error('获取相机配置列表失败:', error)
message.error('获取相机配置列表失败')
console.error('获取相机 IO 列表失败:', error)
}
}
//
const toPtz = (row: CameraIOVO) => {
formData.value.ptzId = row.ptzId
const req = formData.value as unknown as CameraIOVO
CameraIoApi.callPtz(req) // Promise
getList()
}
const addRouters = (id: number) => {
const req = formData.value as unknown as CameraIOVO
CameraIoApi.createCameraIo(req) // Promise
getList()
}
//
const showModelDel = async (id: number) => {
try {
await CameraIoApi.deleteCameraIo(id) // Promise
getList()
} catch (error) {
console.error('删除相机 IO 失败:', error)
}
}
const overwritePtz = (ptzId: number) => {
formData.value.ptzId = ptzId
const req = formData.value as unknown as CameraIOVO
CameraIoApi.overwritePtz(req) // Promise
getList()
}
onMounted(() => {
getList()
})
//
onUnmounted(() => {
//
})

@ -59,7 +59,6 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="id" align="center" prop="id" />
<el-table-column label="名称" align="center" prop="name" />
<el-table-column label="类型" align="center" prop="type">
<template #default="scope">
@ -69,9 +68,6 @@
<el-table-column label="ip" align="center" prop="ip" />
<el-table-column label="端口" align="center" prop="port" />
<el-table-column label="流媒体ip" align="center" prop="rtcServer" />
<el-table-column label="流媒体端口" align="center" prop="rtcServerPort" />
<el-table-column label="rtsp端口" align="center" prop="rtspPort" />
<el-table-column label="录像机ip" align="center" prop="recorderIp" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
@ -86,7 +82,7 @@
link
type="primary"
v-hasPermi="['logistics:camera:update']"
plain @click="dialogTableVisible = true"
plain @click="configurationCameraId(scope.row.id)"
>
配置
</el-button>
@ -110,7 +106,7 @@
/>
<el-dialog v-model="dialogTableVisible" width="900">
<configuration :cameraId="cameraId" />
<configuration :camera-id="cameraId" />
</el-dialog>
</ContentWrap>
@ -131,6 +127,7 @@ defineOptions({ name: 'Camera' })
const dialogTableVisible = ref(false)
const message = useMessage() //
const { t } = useI18n() //
const cameraId = ref(2)
const loading = ref(true) //
const list = ref<CameraVO[]>([]) //
@ -148,7 +145,10 @@ const queryParams = reactive({
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
const configurationCameraId = ((id) => {
cameraId.value = id
dialogTableVisible.value = true
})
/** 查询列表 */
const getList = async () => {
loading.value = true

@ -61,7 +61,7 @@
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item> -->
<el-form-item label="taskid" prop="taskId">
<!-- <el-form-item label="taskid" prop="taskId">
<el-input
v-model="queryParams.taskId"
placeholder="请输入taskid"
@ -69,7 +69,7 @@
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
</el-form-item> -->
<el-form-item label="货架方向" prop="direction">
<el-select
v-model="queryParams.direction"
@ -99,7 +99,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="货位号" prop="storage_Code">
<!-- <el-form-item label="货位号" prop="storage_Code">
<el-input
v-model="queryParams.storage_Code"
placeholder="请输入货位号"
@ -107,7 +107,7 @@
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
</el-form-item> -->
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
@ -135,40 +135,58 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="巷道名" align="center" prop="streetName" />
<el-table-column label="taskid" align="center" prop="taskId" />
<el-table-column label="盘点批次号" align="center" prop="lotnum" />
<el-table-column label="位置" align="center" prop="row" >
<template #default="scope">
<space >{{ scope.row.row+'层-'+scope.row.column+'列' }}</space>
<space >{{ scope.row.streetName+"-"+ (scope.row.direction?"左侧-":"右侧-")+ scope.row.row+'层-'+scope.row.column+'列' }}</space>
</template>
</el-table-column>
<el-table-column label="货位号" align="center" prop="storage_Code" />
<el-table-column label="托盘码" align="center" prop="trayCode" />
<el-table-column label="货架方向" align="center" prop="direction" >
<el-table-column
v-for="(column, index) in columns"
:key="index"
:label="column.remark"
:align="column.align || 'center'"
:prop="column.label"
>
<template #default="scope">
<dict-tag :type="DICT_TYPE.DIRECTION" :value="scope.row.direction" />
<el-button
link
:type='scope.row["wms"+capitalize(column.label)] != scope.row[(column.label)]?"danger":"success"'>
{{ scope.row["wms"+capitalize(column.label)] }}
</el-button>
</template>
</el-table-column>
<!-- <el-table-column label="货架方向" align="center" prop="direction" >
<template #default="scope">
<dict-tag :type="DICT_TYPE.DIRECTION" :value="scope.row.direction" />
</template>
</el-table-column> -->
<el-table-column label="盘点状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CHECK_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作前图片" align="center" prop="pic">
<el-table-column label="时间" align="center"
:formatter="dateFormatter" prop="createTime"/>
<el-table-column label="盘点图片" align="center" prop="pic">
<template #default="scope">
<div class="image">
<el-image
style="width: 60px; height: 50px"
:src="scope.row.pic"
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
:preview-src-list="scope.row.pic"
:initial-index="4"
fit="cover"
/>
</div>
<div v-if="scope.row.urlResources">
<el-image
v-for="(pic, index) in scope.row.urlResources"
:key="index"
:src="pic.url"
style="width: 100px; height: 100px; margin-right: 10px;"
:preview-src-list="[pic.url]"
:initial-index="0"
:preview-teleported="true"
fit="cover"
/>
</div>
<div v-else>
无图片
</div>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
@ -231,12 +249,43 @@ const queryParams = reactive({
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
const getPicList = (pic: any) => {
console.log(pic);
return pic.map.url
}
//
const capitalize = (str: string): string => {
if (!str) return '';
console.log(str);
return str.charAt(0).toUpperCase() + str.slice(1);
};
//
const columns = ref([
{ label: '货物码', prop: 'code' },
{ label: '托盘码', prop: 'trayCode' }
//
]);
/** 查询列表 */
const getColumns = async () => {
loading.value = true
try {
const data = await CheckLogApi.getColumns()
console.log(data);
columns.value = data
} finally {
loading.value = false
}
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await CheckLogApi.getCheckLogPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
@ -308,6 +357,7 @@ const getStringList = () => {
/** 初始化 **/
onMounted(() => {
getList()
getColumns()
getStringList()
})

@ -122,8 +122,8 @@
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="巷道编码" align="center" prop="srmNumber" />
<el-table-column label="随行工单号" align="center" prop="taskId" />
<!-- <el-table-column label="巷道编码" align="center" prop="srmNumber" />
<el-table-column label="方向" align="center" prop="fromDirection">
<template #default="scope">
<dict-tag :type="DICT_TYPE.DIRECTION" :value="scope.row.fromDirection" />
@ -133,13 +133,43 @@
<template #default="scope">
<space >{{ scope.row.fromRow+'层-'+scope.row.fromColumn+'列' }}</space>
</template>
</el-table-column> -->
<el-table-column label="位置" align="center" prop="row" >
<template #default="scope">
<space >{{ scope.row.srmNumber+"-"+ (scope.row.toDirection?"左侧-":"右侧-")+ scope.row.toRow+'层-'+scope.row.toColumn+'列' }}</space>
</template>
</el-table-column>
<el-table-column label="图片" align="center" prop="fromSeparation" >
//
<el-table-column label="图片" align="center" prop="pics" >
<template #default="scope">
<div v-if="scope.row.pics">
<el-image
v-for="(pic, index) in scope.row.pics.split(';')"
:key="index"
:src="pic"
style="width: 100px; height: 100px; margin-right: 10px;"
:preview-src-list="[pic]"
:initial-index="0"
:preview-teleported="true"
fit="cover"
/>
</div>
<div v-else>
无图片
</div>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
v-if="scope.row.videoPath1!=null"
link
type="primary"
@click="videoPlay(scope.row.videoPath2,scope.row.videoPath2)"
v-hasPermi="['logistics:order:delete']"
>
视频
</el-button>
<el-button
link
type="danger"
@ -148,6 +178,7 @@
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
@ -158,6 +189,29 @@
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<el-dialog
v-model="dialogVisible"
title="视频"
width="500"
>
<video ref="videoPlayer" width="100%" controls autoplay muted>
<source :src="video1Src" type="video/mp4" />
您的浏览器不支持 video 标签
</video>
<video ref="videoPlayer" width="100%" controls autoplay muted>
<source :src="video2Src" type="video/mp4" />
您的浏览器不支持 video 标签
</video>
<!--
<video muted autoplay="true" >
<source
:src="video2Src.value"
type="video/mp4"
>
</video> -->
</el-dialog>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
@ -165,6 +219,7 @@
</template>
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { StreetApi,StreetVO } from '@/api/logistics/street'
@ -180,6 +235,7 @@ defineOptions({ name: 'Order' })
const message = useMessage() //
const { t } = useI18n() // ist
const dialogVisible = ref(false)
const streetList = ref<StreetVO[]>([]) //
@ -208,6 +264,15 @@ const queryParams = reactive({
const queryFormRef = ref() //
const exportLoading = ref(false) //
const video1Src = ref("") //
const video2Src = ref("") //
const videoPlay = ((video1,video2) =>{
console.log(video1,video2);
dialogVisible.value = true
video1Src.value = video1
video2Src.value = video2
})
/** 查询列表 */
const getList = async () => {
loading.value = true

@ -48,44 +48,95 @@
<!-- 按钮墙 -->
<ContentWrap>
<!-- 按钮墙 -->
<el-tabs :tab-position="top" style="height: 100%" class="demo-tabs">
<el-tab-pane v-for="(item,index) in columnMap" @click="handleReset(index)" :key="index" :label="item">
<el-row v-for="(row, rowIndex) in rows"
:key="'row-' + rowIndex" >
<el-col :span="1">{{row +"层"}}</el-col>
<el-col :span="1" v-for="(colum, colIndex) in handleReset(index).value" :key="'col-' + colIndex">
<el-button
style="width: 100%;"
:type="getButtonType(row,colum)"
:key="'button-' + colIndex"
plain
@click="handleButtonClick(colum)"
>
{{ row +"-"+colum+"" }}
</el-button>
</el-col>
</el-row>
<el-tabs :tab-position="top" style="height: 100%" class="demo-tabs">
<el-tab-pane v-for="(item, index) in columnMap" :key="index" :label="item">
<el-row v-for="(row, rowIndex) in rows" :key="'row-' + rowIndex">
<el-col :span="1">{{ row + "层" }}</el-col>
<el-col :span="1" v-for="(colum, colIndex) in columnLists[index]" :key="'col-' + colIndex">
<el-button
style="width: 90%;"
:type="getButtonType(row, colum)"
:key="'button-' + colIndex"
:disabled="isDisabled(row, colum)"
@click="getDialogVisible(row, colum)"
>
{{ row + "-" + colum }}
</el-button>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</ContentWrap>
<ContentWrap>
<el-row>
<el-col :span="12">
<el-card style="max-width: 480px;width: 100%;">
<p>统计信息</p>
<!-- ECharts 准备一个定义了宽高的 DOM -->
<Echart :height="300" :options="option" />
</el-card>
</el-col>
<el-col :span="12">
<el-card style="max-width: 480px;width: 35vw;height: 100%;">
<!-- ECharts 准备一个定义了宽高的 DOM -->
<!-- 遍历 Map 中的每个 key-value -->
<!-- 遍历 Map 中的每个 key-value -->
<div v-for="(title) in buttonByObjectMap.keys()" :key="title">
<!-- 标题 -->
<h3>{{ title }}</h3>
<!-- 遍历每个标题下的按钮数组 -->
<el-space :size="size" :spacer="spacer">
<div
class="mb-15px"
style="width: 5%;" v-for="(button, index) in buttonByObjectMap.get(title)" :key="index">
<el-button
:type="getButtonType(button.row, button.column)"
:key="'button2-' + index"
:disabled="isDisabled(button.row, button.column)"
@click="getDialogVisible(button.row, button.column)"
>
{{ button.row }}-{{ button.column }}
</el-button>
</div>
</el-space>
</div>
</el-card>
</el-col>
</el-row>
</ContentWrap>
<el-dialog
v-model="dialogVisible"
:title="getStreetName()+getDirectionName()+'侧'+rowName+'层'+columnName+'列'"
width="40%"
@close="handleClose"
>
<stock :stockId="getStockId()" :key="getStockId()"/>
</el-dialog>
</template>
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { StreetApi,StreetVO } from '@/api/logistics/street'
import { StockApi,StockVO } from '@/api/logistics/stock'
import { da, el, ro } from 'element-plus/es/locale'
import stock from '@/views/logistics/stock/stock.vue'
import { forEach } from 'lodash-es'
import { EChartsOption } from 'echarts'
import { log } from 'console'
import Cookies from 'js-cookie';
const message = useMessage() //
const { t } = useI18n() // ist
@ -96,10 +147,40 @@ let columns = ref(48);
const rowMax = ref(20);
const columnMax = ref(20);
const columnList = ref<number[]>([]);
const rowName = ref("")
const columnName = ref("")
const currentStockId =ref(0)
let columnMap = ref<String[]>([]);
const buttons = ref<ButtonVo[]>([]);
const dialogVisible = ref(false)
const getDialogVisible = (row,colum) => {
clearTimer()
rowName.value = row
columnName.value = colum
dialogVisible.value = true
}
// Cookie queryParams
const loadQueryParamsFromCookie = () => {
const savedQueryParams = Cookies.get('queryParams');
if (savedQueryParams) {
const parsedParams = JSON.parse(savedQueryParams);
Object.assign(queryParams, parsedParams); // reactive
}
};
const columnLists = computed(() => {
return columnMap.value.map((_, index) => {
const minIndex = index * columnMax.value + 1;
const maxIndex = (index + 1) * columnMax.value < columns.value ? (index + 1) * columnMax.value : columns.value;
const columnList = [];
for (let i = minIndex; i <= maxIndex; i++) {
columnList.push(i);
}
return columnList;
});
});
const loading = ref(true) //
const total = ref(0) //
@ -138,15 +219,20 @@ interface ButtonVo {
checkId?: number;
statusVision?: string;
}
// queryParams Cookie
const saveQueryParamsToCookie = () => {
Cookies.set('queryParams', JSON.stringify(queryParams), { expires: 7 }); // 7
};
/** 搜索按钮操作 */
const handleQuery = async () => {
columnMap.value = []
const data = await StockApi.getStreetList(queryParams)
saveQueryParamsToCookie()
getStreetStatus()
buttons.value = data.buttons
rows = ref(data.rows)
columns =ref(data.columns)
console.log(rows);
const page = columns.value/columnMax.value
for(var i = 0; i <= page; i++){
@ -155,28 +241,22 @@ const handleQuery = async () => {
}
const getButtonType = (row: number, colum: number) => {
let info = "info";
if (!buttons.value || buttons.value.length === 0) {
return info;
}
for (const button of buttons.value) {
if (button.row === row && button.column === colum) {
//
if(button.status === '0'){
}else if(button.status === '1'){
info = "danger"
}else if(button.status === '2' || button.status === '3'){
info = "success"
}
break
}
}
return info
const handleClose = () => {
dialogVisible.value = false
startTimer()
}
const isDisabled = (row: number, column: number): boolean => {
return !buttonMap.value.has(`${row}-${column}`);
};
const getStockId = (): string => {
return buttonByIdMap.value.get(`${rowName.value}-${columnName.value}`) ||"";
};
const getButtonType = (row: number, column: number): string => {
return buttonMap.value.get(`${row}-${column}`) ||"";
};
const handleReset = (index) => {
columnList.value = []
const minIndex = (index) *columnMax.value +1
@ -189,7 +269,54 @@ const handleReset = (index) => {
}
// getStreetStatus
const buttonMap = ref(new Map<string, string>());// row-column status
// getStreetStatus
const buttonByIdMap = ref(new Map<string, string>());// row-column stockId
const buttonByStatusMap = ref(new Map<string, number>());// row-column statusString
const buttonByObjectMap = ref(new Map<string, Array<object>>());
const getStreetStatus = async () => {
buttonMap.value = new Map()
buttonByIdMap.value = new Map()
buttonByStatusMap.value = new Map()
buttonByObjectMap.value = new Map()
const data = await StockApi.getStreetStatus(queryParams)
for(var i = 0; i < data.length; i++){
buttonMap.value.set(`${data[i].row}-${data[i].column}`, data[i].colour)
buttonByIdMap.value.set(`${data[i].row}-${data[i].column}`, data[i].id)
if(buttonByStatusMap.value.has(data[i].statusString)){
buttonByStatusMap.value.set(data[i].statusString, buttonByStatusMap.value.get(data[i].statusString)!+1)
buttonByObjectMap.value.get(data[i].statusString)?.push(data[i])
}else{
buttonByStatusMap.value.set(data[i].statusString, 1)
buttonByObjectMap.value.set(data[i].statusString, [data[i]])
}
}
console.log(buttonByObjectMap.value);
}
// buttonByStatusMap
watch(buttonByStatusMap, (newMap) => {
const echartData = Array.from(newMap, ([name, value]) => ({ name, value }))
option.series[0].data = echartData
}, { deep: true })
const streetList = ref<StreetVO[]>([]) //
const getStreetName = ()=>{
return streetList.value.find(item => item.id === queryParams.streetId)?.name
}
const getDirectionName = ()=>{
return getIntDictOptions(DICT_TYPE.DIRECTION).find(item => item.value === queryParams.direction)?.label
}
/** 查询巷道列表 */
const getStringList = () => {
@ -198,21 +325,93 @@ const getStringList = () => {
data.then((res)=>{
streetList.value = res
})
console.log(data);
console.log(streetList);
} finally {
}
}
// ECharts 使 statusString name
const echartData =(()=>{
return Array.from(buttonByStatusMap.value, ([name, value]) => ({ name, value }));
}
);
//
let timer: NodeJS.Timeout | null = null
const startTimer = () => {
timer = setInterval(() => {
//
//
getStreetStatus()
}, 15000) //
}
const clearTimer = () => {
if (timer) {
clearInterval(timer)
timer = null
}
}
// ECharts 使 statusString name
const option = reactive({
tooltip: {
trigger: 'item'
},
legend: {
top: '5%',
left: 'center'
},
series: [
{
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 20,
fontWeight: 'bold'
}
},
labelLine: {
show: false
}, //
normal: {
color: function(params) {
// params.name
const colorList = ['#87cefa', '#da70d6', '#32cd32', '#ff6347'];
return colorList[params.dataIndex]; // index
}
},
data: echartData()
}
]
});
/** 初始化 **/
onMounted(() => {
handleQuery()
getStringList()
loadQueryParamsFromCookie()
startTimer() //
handleReset(0)
})
onUnmounted(() => {
clearTimer() //
})
</script>

@ -0,0 +1,298 @@
<template>
<div class="app">
<!-- <div v-if="visible" class="modal-overlay" @click="closeBoxCard">
<div class="modal-content" @click.stop> -->
<!-- 跑马灯容器 -->
<div class="carousel-container">
<el-carousel v-if="!loading" height="500px" arrow="always">
<el-carousel-item v-for="(image, index) in carouselImages" :key="index">
<!-- 添加双击事件和放大容器 -->
<el-image
class="carousel-image"
:src="image.url"
:fit="cover"
@dblclick="showEnlargedImage(image.url)"
/>
<div class="text-overlay">{{ image.little }}</div>
</el-carousel-item>
</el-carousel>
<div v-else class="loading-text">加载中...</div>
</div>
<!-- 放大图片模态框 -->
<div v-if="showEnlarged" class="enlarged-modal" @click="showEnlarged = false">
<img :src="enlargedImageUrl" class="enlarged-image" />
</div>
<div class="box-card">
<div class="card-body">
<div class="info-container">
<div v-for="(item, index) in stockInfo" :key="index" class="info-item" :class="{ 'status-error': item.label === '盘点结果' && item.value === '盘点异常' }">
<strong>{{ item.label }}:</strong>
<span>{{ item.value }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- </div>
</div> -->
</template>
<script>
import { ref, onMounted } from 'vue';
import { StockApi } from '@/api/logistics/stock';
export default {
props: {
stockId: {
type: [String, Number],
default: null
}
},
setup(props, { emit }) {
const loading = ref(true);
const carouselImages = ref([]);
const stockInfo = ref([]);
const showEnlarged = ref(false);
const enlargedImageUrl = ref('');
//
const fetchStockInfo = async () => {
try {
const response = await StockApi.getStockStatus({
stockId: props.stockId
});
let statusString = "盘点正确";
const scanData = response.scan || [];
//
// const hasInconsistent = scanData.some(item =>
// item.wmsCode !== item.code
// );
// if (hasInconsistent) {
// statusString = "";
// }
//
carouselImages.value = response.images || [];
//
const formatDate = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
};
//
stockInfo.value = [
{
label: '创建时间',
value: formatDate(response.createTime) // 使
},
{
label: '批次号',
value: response.lotnum || '-'
},
{
label: '盘点结果',
value: (response.statusString)
},
// {
// label: '',
// value: response.trayCode || '-'
// }
];
console.log(response.scan);
// stockInfo
for(const item of response.scan){
//wms
stockInfo.value.push({
label: '上位'+item.remark,
value: item.wmsCode
});
//
stockInfo.value.push({
label: '扫描'+item.remark,
value: item.code
});
//
stockInfo.value.push({
label: '扫描结果',
value: item.code===item.wmsCode?'正确':'异常'
});
}
console.log(stockInfo);
} catch (error) {
console.error('获取库存信息失败:', error);
} finally {
loading.value = false;
}
};
onMounted(() => {
console.log(props.stockId);
if (props.stockId) {
fetchStockInfo();
}
});
const showEnlargedImage = (url) => {
enlargedImageUrl.value = url;
showEnlarged.value = true;
};
const closeBoxCard = () => {
emit('close-box-card');
};
return {
loading,
carouselImages,
stockInfo,
showEnlarged,
enlargedImageUrl,
showEnlargedImage,
closeBoxCard
};
}
};
</script>
<style scoped>
/* 新增放大图片样式 */
.enlarged-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
cursor: pointer;
}
.enlarged-image {
max-width: 90%;
max-height: 90%;
object-fit: contain;
}
/* 原有样式保持不变 */
.app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #fff;
padding: 20px;
border-radius: 8px;
width: 80%;
max-width: 600px;
}
.status-error {
color: red;
}
<style scoped>
.box-card {
z-index: 1; /* 确保在父容器之上 */
background-color: #fff;
padding: 20px;
border-radius: 8px;
width: 100%;
max-width: 600px;
position: relative;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.carousel-image {
width: 100%;
height: 100%;
object-fit: cover; /* 图片填充容器,并保持比例 */
object-position: center; /* 确保图片居中 */
}
.close-btn {
background-color: transparent;
border: none;
font-size: 18px;
cursor: pointer;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-body {
font-size: 12px;
}
.info-container {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.info-item {
flex: 1 1 calc(25% - 01px);
background-color: #f9f9f9;
padding: 10px;
border-radius: 4px;
box-sizing: border-box;
text-align: center;
}
.carousel-container {
position: relative; /* 或 absolute、fixed、sticky根据你的布局需求选择 */
/* z-index: 9999; */
width: 100%;
max-width: 888px;
padding: 2px;
margin: 0 auto;
}
.carousel-image {
width: 100%;
height: auto;
display: block;
cursor: pointer;
box-sizing: border-box;
}
.text-overlay {
position: absolute;
top: 20px;
left: 10px;
color: white;
font-size: 30px;
}
</style>
Loading…
Cancel
Save