提交 ac045689 authored 作者: 王鹏飞's avatar 王鹏飞

feat: 页面响应式设计

上级 608dd550
......@@ -83,3 +83,18 @@ textarea:focus {
margin-right: 0 !important;
border-bottom: 1px solid #e6e6e6;
}
/* ========== 全局响应式 ========== */
img, video { max-width: 100%; height: auto; }
@media (max-width: 767px) {
.el-dialog { width: 92% !important; max-width: 500px; margin-top: 10vh !important; }
.el-message-box { width: 90% !important; max-width: 420px; }
.el-drawer { width: 90% !important; }
.el-form--inline .el-form-item { margin-right: 0; }
.el-table { font-size: 13px; }
}
@media (min-width: 768px) { .hide-on-desktop { display: none !important; } }
@media (max-width: 767px) { .hide-on-mobile { display: none !important; } }
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import { useMediaQuery, useStorage } from '@vueuse/core'
const emit = defineEmits<{
(e: 'resize'): void
}>()
defineProps<{ isLeftShow?: boolean }>()
const isMobile = useMediaQuery('(max-width: 767px)')
const leftPanelVisible = $ref<boolean>(true)
const leftPanelWidth = useStorage('leftPanelWidth', 400)
......@@ -17,6 +18,7 @@ const leftPanelWidthText = $computed(() => {
// 因为鼠标移动文档和iframe影响不能move
let dragFlag = $ref(false)
function useDrag() {
if (isMobile.value) return
const dragDom = document.getElementById('panel-resize')
if (dragDom) {
dragDom.addEventListener('mousedown', (e) => {
......@@ -47,8 +49,8 @@ onMounted(() => {
<div class="drag-panel-left-content">
<slot name="left"></slot>
</div>
<div class="panel-resize" id="panel-resize"></div>
<div class="panel-icon" @click="leftPanelVisible = !leftPanelVisible">
<div class="panel-resize" id="panel-resize" v-if="!isMobile"></div>
<div class="panel-icon" @click="leftPanelVisible = !leftPanelVisible" v-if="!isMobile">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 86" aria-hidden="true" width="16" height="86">
<g fill="none" fill-rule="evenodd">
<path
......@@ -141,4 +143,25 @@ onMounted(() => {
z-index: 999;
opacity: 0;
}
@media (max-width: 767px) {
.drag-panel {
flex-direction: column;
height: auto;
gap: 12px;
}
.drag-panel-left {
width: 100% !important;
padding: 12px;
margin-right: 0;
}
.drag-panel-left-content {
height: auto;
overflow: visible;
}
.drag-panel-right {
width: 100%;
height: auto;
min-width: 0;
}
}
</style>
<script setup lang="ts">
import { useMediaQuery } from '@vueuse/core'
interface Props {
url: string
}
const props = defineProps<Props>()
const isMobile = useMediaQuery('(max-width: 767px)')
// 文件扩展名
const fileExtensionName = $computed(() => {
return props.url.split('.').pop() || ''
})
const isOfficeFile = $computed(() => {
return ['pptx', 'doc', 'docx', 'xls', 'xlsx'].includes(fileExtensionName)
})
// office文件
const officeUrl = $computed(() => {
return ['pptx', 'doc', 'docx', 'xls', 'xlsx'].includes(fileExtensionName)
return isOfficeFile
? `https://view.officeapps.live.com/op/view.aspx?src=${props.url}`
: ''
})
const fileName = $computed(() => {
const [path] = props.url.split('?')
return decodeURIComponent(path.split('/').pop() || '文档')
})
</script>
<template>
<iframe allowfullscreen :src="officeUrl" :key="officeUrl" frameborder="0" style="width: 100%; height: 100%" v-if="officeUrl"></iframe>
<div v-if="isMobile && officeUrl" class="preview-mobile-office">
<div class="preview-mobile-office__title">{{ fileName }}</div>
<div class="preview-mobile-office__desc">移动端不内嵌 Office 文档预览,点击后在新页面打开</div>
<a :href="officeUrl" target="_blank" rel="noopener noreferrer" class="preview-mobile-office__link">打开文档</a>
</div>
<iframe allowfullscreen :src="officeUrl" :key="officeUrl" frameborder="0" style="width: 100%; height: 100%" v-else-if="officeUrl"></iframe>
<object :data="url" style="width: 100%; height: 100%; object-fit: none" v-else></object>
</template>
<style lang="scss" scoped>
.preview-mobile-office {
height: 100%;
min-height: 240px;
padding: 24px 16px;
background: #fff;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.preview-mobile-office__title {
font-size: 16px;
font-weight: 600;
line-height: 24px;
color: #333;
word-break: break-word;
}
.preview-mobile-office__desc {
margin-top: 8px;
font-size: 14px;
line-height: 22px;
color: #666;
}
.preview-mobile-office__link {
margin-top: 16px;
min-width: 120px;
padding: 10px 20px;
border-radius: 999px;
background: var(--main-color);
color: #fff;
line-height: 20px;
}
</style>
<script setup lang="ts">
import { Search, RefreshLeft } from '@element-plus/icons-vue'
import { useMediaQuery } from '@vueuse/core'
const isMobile = useMediaQuery('(max-width: 767px)')
const cardColumns = computed(() => props.columns.filter((c) => c.type !== 'index' && c.type !== 'selection' && c.slots !== 'table-x'))
const hasSelectionColumn = computed(() => props.columns.some((c) => c.type === 'selection'))
const hasActionsColumn = computed(() => props.columns.some((c) => c.slots === 'table-x'))
interface IRemoteProps {
params?: any
......@@ -16,13 +23,17 @@ interface Props {
data?: any[]
hasPagination?: boolean
limit?: number
hasLoading?: boolean
mobileCards?: boolean
}
const props = withDefaults(defineProps<Props>(), {
hasPagination: true,
hasLoading: true,
limit: 10,
filters: () => [],
columns: () => [],
data: () => []
data: () => [],
mobileCards: true,
})
const filterFormRef = ref()
......@@ -32,97 +43,64 @@ const dataList = ref<any[]>([])
const page = reactive({ total: 0, size: props.limit, currentPage: 1 })
const params = reactive({ ...props.remote?.params })
watchEffect(() => {
Object.assign(params, props.remote?.params)
})
watchEffect(() => {
dataList.value = props.data || []
})
watchEffect(() => { Object.assign(params, props.remote?.params) })
watchEffect(() => { dataList.value = props.data || [] })
// 获取数据
const fetchList = (isReset = false) => {
/**
* @param function httpRequest api接口
* @param function beforeRequest 接口请求之前
* @param function callback 接口请求成功回调
*/
const { httpRequest, beforeRequest, callback } = props.remote || {}
if (!httpRequest) {
return
}
// 参数设置
if (!httpRequest) return
let requestParams = { ...params }
// 翻页参数设置
if (props.hasPagination) {
requestParams.page = page.currentPage
requestParams['per-page'] = page.size
}
// 接口请求之前
if (beforeRequest) {
requestParams = beforeRequest(requestParams, isReset)
}
if (beforeRequest) requestParams = beforeRequest(requestParams, isReset)
for (const key in params) {
if (params[key] === '' || params[key] === undefined || params[key] === undefined) {
delete params[key]
}
if (params[key] === '' || params[key] === undefined) delete params[key]
}
loading.value = true
return (
httpRequest(requestParams)
return httpRequest(requestParams)
.then((res: any) => {
const { list = [], total = 0 } = callback ? callback(res.data, requestParams) : res.data || {}
page.total = parseInt(total)
dataList.value = list
})
// .catch(() => {
// page.total = 0
// dataList.value = []
// })
.finally(() => {
loading.value = false
})
)
.finally(() => { loading.value = false })
}
// 搜索
const search = () => {
page.currentPage = 1
return fetchList()
}
const search = () => { page.currentPage = 1; return fetchList() }
const reset = () => { filterFormRef.value?.resetFields(); page.currentPage = 1; return fetchList(true) }
const refetch = (isForce = false) => isForce ? reset() : fetchList()
const pageSizeChange = (value: number) => { page.currentPage = 1; page.size = value; fetchList() }
// 重置
const reset = () => {
// 清空筛选条件
filterFormRef.value?.resetFields()
// 初始化页码
page.currentPage = 1
// 刷新列表
return fetchList(true)
// 移动端多选
const mobileSelection = ref<any[]>([])
const mobileIsAllSelected = computed(() => dataList.value.length > 0 && mobileSelection.value.length === dataList.value.length)
function toggleMobileSelectAll() {
mobileSelection.value = mobileIsAllSelected.value ? [] : [...dataList.value]
emitSelectionChange()
}
// 刷新
const refetch = (isForce = false) => {
return isForce ? reset() : fetchList()
function toggleMobileSelectRow(row: any) {
const idx = mobileSelection.value.findIndex((r) => r === row)
if (idx > -1) mobileSelection.value.splice(idx, 1)
else mobileSelection.value.push(row)
emitSelectionChange()
}
// 页数改变
const pageSizeChange = (value: number) => {
page.currentPage = 1
page.size = value
fetchList()
function isMobileRowSelected(row: any) { return mobileSelection.value.some((r) => r === row) }
function emitSelectionChange() {
const attrs = useAttrs()
const handler = (attrs as Record<string, unknown>).onSelectionChange as ((val: any[]) => void) | undefined
if (handler) handler(mobileSelection.value)
}
onMounted(() => {
fetchList()
})
onMounted(() => { fetchList() })
defineExpose({ refetch, tableRef })
</script>
<template>
<div class="table-list">
<div class="table-list-hd">
<!-- 筛选 -->
<div class="table-list-filter" v-if="filters && filters.length">
<el-form :inline="true" :model="params" v-bind="filterForm" ref="filterFormRef" @submit.prevent>
<template v-for="item in filters" :key="item.prop">
......@@ -131,15 +109,10 @@ defineExpose({ refetch, tableRef })
<slot :name="item.slots" v-bind="{ params }"></slot>
</template>
<template v-else>
<!-- input -->
<el-input v-model="params[item.prop]" v-bind="item" clearable @change="search" style="width: 200px" v-if="item.type === 'input'" />
<!-- select -->
<el-select v-model="params[item.prop]" v-bind="item" filterable clearable @change="search" v-if="item.type === 'select'">
<el-option
:label="option[item.labelKey] || option.label"
:value="option[item.valueKey] || option.value"
v-for="(option, index) in item.options"
:key="index" />
<el-option :label="option[item.labelKey] || option.label" :value="option[item.valueKey] || option.value"
v-for="(option, index) in item.options" :key="index" />
</el-select>
</template>
</el-form-item>
......@@ -150,14 +123,15 @@ defineExpose({ refetch, tableRef })
</el-form-item>
</el-form>
</div>
<!-- <div class="table-list-hd-aside"><slot name="header-aside" /></div> -->
</div>
<div class="table-list-buttons"><slot name="header-buttons"></slot></div>
<slot></slot>
<!-- 主体 -->
<div class="table-list-bd">
<slot name="body" v-bind="{ data: dataList }">
<el-table stripe :header-cell-style="{ background: '#ededed' }" :data="dataList" v-loading="loading" v-bind="$attrs" ref="tableRef">
<!-- 桌面端表格 -->
<el-table v-if="!isMobile || !mobileCards" stripe :header-cell-style="{ background: '#ededed' }"
:data="dataList" v-loading="loading && hasLoading" v-bind="$attrs" ref="tableRef">
<el-table-column align="center" v-bind="item || {}" v-for="item in columns" :key="item.prop">
<template #default="scope" v-if="item.slots || item.computed">
<slot :name="item.slots" v-bind="scope" v-if="item.slots"></slot>
......@@ -165,74 +139,101 @@ defineExpose({ refetch, tableRef })
</template>
</el-table-column>
</el-table>
<!-- 移动端卡片 -->
<div v-if="isMobile && mobileCards" class="mobile-card-list" v-loading="loading && hasLoading">
<div class="mobile-card-select-all" v-if="hasSelectionColumn && dataList.length">
<el-checkbox :model-value="mobileIsAllSelected" @change="toggleMobileSelectAll">
全选({{ mobileSelection.length }}/{{ dataList.length }})
</el-checkbox>
</div>
<div class="mobile-card" v-for="(row, rowIndex) in dataList" :key="rowIndex"
:class="{ 'is-selected': isMobileRowSelected(row) }">
<div class="mobile-card-body">
<div class="mobile-card-checkbox" v-if="hasSelectionColumn" @click.stop="toggleMobileSelectRow(row)">
<el-checkbox :model-value="isMobileRowSelected(row)" />
</div>
<div class="mobile-card-rows">
<div class="mobile-card-row" v-for="col in cardColumns" :key="col.prop || col.label">
<span class="mobile-card-label">{{ col.label }}</span>
<span class="mobile-card-colon">:</span>
<span class="mobile-card-value">
<slot v-if="col.slots && $slots[col.slots]" :name="col.slots" v-bind="{ row, $index: rowIndex }"></slot>
<div v-html="col.computed({ row, $index: rowIndex })" v-else-if="col.computed"></div>
<template v-else>{{ row[col.prop] ?? '-' }}</template>
</span>
</div>
</div>
</div>
<div class="mobile-card-ft" v-if="hasActionsColumn">
<slot name="table-x" v-bind="{ row, $index: rowIndex }"></slot>
</div>
</div>
<el-empty v-if="!dataList.length && !loading" description="暂无数据" />
</div>
</slot>
</div>
<!-- 底部 -->
<div class="table-list-ft">
<div>
<slot name="footer"></slot>
</div>
<el-pagination
class="table-list-pagination"
background
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[10, 20, 30, 50]"
:page-size="page.size"
:total="page.total"
v-model:currentPage="page.currentPage"
@size-change="pageSizeChange"
@current-change="fetchList()"
:hide-on-single-page="true"
v-if="hasPagination">
<div><slot name="footer"></slot></div>
<el-pagination class="table-list-pagination" background
:layout="isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
:page-sizes="[10, 20, 30, 50]" :page-size="page.size" :total="page.total"
:pager-count="isMobile ? 5 : 7"
v-model:currentPage="page.currentPage" @size-change="pageSizeChange" @current-change="fetchList()"
:hide-on-single-page="true" v-if="hasPagination">
</el-pagination>
</div>
</div>
</template>
<style lang="scss">
// .table-list {
// height: 100%;
// display: flex;
// flex-direction: column;
// box-sizing: border-box;
// }
.table-list-hd {
display: flex;
margin-bottom: 20px;
&:empty {
display: none;
}
.table-list-hd { display: flex; margin-bottom: 20px; &:empty { display: none; } }
.table-list-filter { flex: 1; }
.table-list-buttons { margin-bottom: 20px; display: flex; flex-wrap: wrap; gap: 8px; &:empty { display: none; } }
.table-list-ft { padding: 10px 0; display: flex; align-items: center; justify-content: space-between; }
.table-list-pagination { text-align: right; }
.el-table-column--selection .cell { padding: 0 14px !important; }
.table-list-bd .el-button {
a { margin: -8px -15px; padding: 8px 15px; }
&.is-text { padding: 8px; }
&.is-text + .is-text { margin-left: 0; }
&.is-plain { --el-button-border-color: var(--el-button-bg-color); }
}
.table-list-filter {
flex: 1;
// padding: 30px 30px 10px;
// background: #f8f8f8;
// border-radius: 12px;
}
.table-list-buttons {
margin-bottom: 20px;
&:empty {
display: none;
}
}
// .table-list-bd {
// flex: 1;
// }
.table-list-ft {
padding: 10px 0;
display: flex;
align-items: center;
justify-content: space-between;
// 移动端卡片
.mobile-card-list { min-height: 120px; }
.mobile-card-select-all { padding: 8px 0; .el-checkbox { font-size: 13px; color: #666; } }
.mobile-card { background: #fff; border-radius: 8px; border: 1px solid #eee; margin-bottom: 12px; overflow: hidden; display: flex; flex-direction: column;
&.is-selected { border-color: var(--main-color); background: #fef5f7; }
}
.table-list-pagination {
text-align: right;
.mobile-card-body { flex: 1; padding: 14px 16px; display: flex; align-items: flex-start; gap: 10px; }
.mobile-card-checkbox { flex-shrink: 0; padding-top: 2px; }
.mobile-card-rows { flex: 1; min-width: 0; }
.mobile-card-row { display: flex; padding: 5px 0; border-bottom: 1px solid #f5f5f5; line-height: 1.6;
&:last-child { border-bottom: none; }
}
.el-table-column--selection .cell {
padding: 0 14px !important;
.mobile-card-label { flex-shrink: 0; color: #999; font-size: 14px; }
.mobile-card-colon { color: #999; font-size: 14px; margin-right: 6px; }
.mobile-card-value { flex: 1; font-size: 14px; color: #333; min-width: 0; word-break: break-all; }
.mobile-card-ft { padding: 8px 16px; display: flex; gap: 2px; flex-wrap: wrap; background: #fafafa; border-top: 1px solid #f0f0f0;
.el-button.is-text { padding: 4px 6px; font-size: 12px; }
}
.el-button a {
margin: -8px -15px;
padding: 8px 15px;
// 移动端
@media (max-width: 767px) {
.table-list-hd .el-form-item__label { display: none; }
.table-list-filter {
.el-form, .el-form--inline { width: 100%; }
.el-form-item { width: 100%; margin-right: 0; margin-bottom: 8px; }
.el-form-item__content { width: 100%; }
.el-input, .el-select, .el-cascader { width: 100% !important; }
.filter-buttons { width: auto; }
}
.table-list-ft { flex-direction: column; gap: 8px; padding: 8px 0; }
.table-list-pagination .el-pagination {
justify-content: center;
}
.el-pager .number { min-width: 28px; height: 28px; line-height: 28px; font-size: 13px; }
.btn-prev, .btn-next { height: 28px; min-width: 28px; }
}
</style>
......@@ -82,6 +82,13 @@ function handleClick(path: string) {
flex: 0 0 200px;
box-sizing: content-box;
}
// 移动端 overlay
.app-aside--overlay {
position: fixed; top: 0; left: 0; bottom: 0; z-index: 2000;
width: 220px; flex: none;
transform: translateX(-100%); transition: transform 0.3s ease;
&.app-aside--visible { transform: translateX(0); }
}
.nav {
position: fixed;
width: 200px;
......@@ -132,4 +139,10 @@ function handleClick(path: string) {
// border-radius: 8px;
// }
}
// 平板收缩
@media (max-width: 1024px) {
.app-aside:not(.app-aside--overlay) { width: 180px; flex: 0 0 180px; }
.nav { width: 180px; }
}
</style>
......@@ -17,4 +17,7 @@ export default { name: 'AppFooter' }
text-align: center;
background-color: #fff;
}
@media (max-width: 767px) {
.app-footer { padding: 8px 12px; font-size: 12px; line-height: 20px; }
}
</style>
......@@ -4,7 +4,7 @@ export default { name: 'AppHeader' }
<script setup lang="ts">
import type { IMenuItem } from '@/types'
import { CaretBottom } from '@element-plus/icons-vue'
import { CaretBottom, Expand } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
import { useMenuStore } from '@/stores/menu'
import { useAppConfig } from '@/composables/useAppConfig'
......@@ -12,6 +12,10 @@ const appConfig = useAppConfig()
const route = useRoute()
const router = useRouter()
const mobileMenuVisible = ref(false)
const toggleMobileMenu = () => { mobileMenuVisible.value = !mobileMenuVisible.value }
const closeMobileMenu = () => { mobileMenuVisible.value = false }
const userStore = useUserStore()
const userInfo = userStore.user
......@@ -60,6 +64,9 @@ function handleClick(path: string) {
<template>
<header class="app-header">
<div class="app-header-left">
<button class="sidebar-toggle" @click="toggleMobileMenu">
<el-icon :size="22"><Expand /></el-icon>
</button>
<template v-if="appConfig.logo">
<div class="logo">
<router-link to="/"><img :src="appConfig.logo" /></router-link>
......@@ -125,6 +132,28 @@ function handleClick(path: string) {
</el-dropdown>
</div>
</header>
<el-drawer v-model="mobileMenuVisible" direction="ltr" :size="260" :with-header="false" :z-index="2100">
<div class="mobile-drawer">
<div class="mobile-drawer-hd">
<img v-if="appConfig.logo" :src="appConfig.logo" class="mobile-drawer-logo" />
<span class="mobile-drawer-title">{{ appConfig.title }}</span>
</div>
<div class="mobile-drawer-bd">
<template v-for="item in menus" :key="item.path">
<div class="mobile-nav-group" v-permission="item.tag">
<div class="mobile-nav-parent" @click="handleClick(item.path)">{{ item.name }}</div>
<template v-if="item.children">
<div class="mobile-nav-child" v-for="subitem in item.children" :key="subitem.path"
v-permission="subitem.tag"
:class="{ 'is-active': route.path.includes(subitem.path) }"
@click="handleClick(subitem.path); closeMobileMenu()">{{ subitem.name }}</div>
</template>
</div>
</template>
</div>
</div>
</el-drawer>
</template>
<style lang="scss">
......@@ -280,4 +309,58 @@ function handleClick(path: string) {
.app-header-user-buttons {
padding-top: 16px;
}
// ========== 响应式 ==========
.sidebar-toggle {
display: none;
align-items: center; justify-content: center;
width: 36px; height: 36px; margin-right: 12px;
border: none; border-radius: 6px;
background: transparent; color: #333; cursor: pointer; flex-shrink: 0;
&:hover { background: #f0f0f0; }
}
// 平板 (≤1024px)
@media (max-width: 1024px) {
.app-header { padding: 0 12px; }
.app-header-left .app-name { font-size: 22px; }
.app-header-nav { margin: 0 20px; --el-menu-item-font-size: 14px;
.el-menu--horizontal > .el-menu-item { margin-right: 24px; }
}
}
// 移动端 (≤767px)
@media (max-width: 767px) {
.sidebar-toggle { display: flex; }
.app-header { padding: 0 12px; height: 52px; }
.app-header-left {
.logo { height: 32px; }
.line { margin: 0 10px; height: 22px; }
.app-name { font-size: 16px; line-height: 22px; }
}
.app-header-nav { display: none; }
.app-header-right .user {
height: 36px;
p { display: none; }
}
.app-header-right .avatar { width: 32px; height: 32px; }
}
// 移动端抽屉
.mobile-drawer { display: flex; flex-direction: column; height: 100%; }
.mobile-drawer-hd { display: flex; align-items: center; gap: 10px; padding: 16px; border-bottom: 1px solid #eee; }
.mobile-drawer-logo { height: 36px; }
.mobile-drawer-title { font-size: 18px; font-weight: 600; color: #333; }
.mobile-drawer-bd { flex: 1; overflow-y: auto; padding: 8px 0; }
.mobile-nav-group { border-bottom: 1px solid #f5f5f5;
&:last-child { border-bottom: none; }
}
.mobile-nav-parent { padding: 14px 20px; font-size: 15px; font-weight: 600; color: #333; cursor: pointer;
&:active { background: #f5f5f5; }
}
.mobile-nav-child { padding: 12px 20px 12px 36px; font-size: 14px; color: #666; cursor: pointer; border-top: 1px solid #fafafa;
&:active { background: #f5f5f5; }
&.is-active { color: var(--main-color); background: #fef5f7; }
}
</style>
......@@ -7,20 +7,30 @@ import AppHeader from './Header.vue'
import AppAside from './Aside.vue'
import AppMain from './Main.vue'
import AppFooter from './Footer.vue'
import { useMediaQuery } from '@vueuse/core'
withDefaults(defineProps<{ sidebar?: boolean; footer?: boolean; header?: boolean }>(), {
const props = withDefaults(defineProps<{ sidebar?: boolean; footer?: boolean; header?: boolean }>(), {
header: true,
sidebar: false,
footer: false
})
const isMobile = useMediaQuery('(max-width: 767px)')
const sidebarVisible = ref(false)
const closeSidebar = () => { sidebarVisible.value = false }
const showOverlaySidebar = computed(() => isMobile.value && sidebarVisible.value && props.sidebar)
</script>
<template>
<div class="app-layout">
<div class="app-layout" :class="{ 'app-layout--mobile': isMobile }">
<AppHeader v-if="header"></AppHeader>
<div class="app-layout-container">
<AppAside v-if="sidebar"></AppAside>
<AppMain></AppMain>
<div v-if="showOverlaySidebar" class="sidebar-overlay" @click="closeSidebar"></div>
<AppAside
v-if="sidebar"
:class="{ 'app-aside--overlay': isMobile, 'app-aside--visible': sidebarVisible }">
</AppAside>
<AppMain @click="closeSidebar"></AppMain>
</div>
<AppFooter v-if="footer"></AppFooter>
</div>
......@@ -28,7 +38,6 @@ withDefaults(defineProps<{ sidebar?: boolean; footer?: boolean; header?: boolean
<style lang="scss">
.app-layout {
min-width: 1000px;
min-height: 100vh;
margin: 0 auto;
background-color: #ecf2f7;
......@@ -36,8 +45,18 @@ withDefaults(defineProps<{ sidebar?: boolean; footer?: boolean; header?: boolean
flex-direction: column;
}
.app-layout-container {
// min-height: calc(100vh - 66px);
flex: 1;
display: flex;
position: relative;
}
.sidebar-overlay {
display: none;
}
.app-layout--mobile .sidebar-overlay {
display: block;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 1999;
}
</style>
......@@ -16,6 +16,7 @@ export default { name: 'AppMain' }
flex: 1;
padding: 20px;
overflow: hidden;
min-width: 0;
}
.app-main-inner {
height: 100%;
......@@ -26,4 +27,6 @@ export default { name: 'AppMain' }
display: table;
clear: both;
}
@media (max-width: 1024px) { .app-main { padding: 16px; } }
@media (max-width: 767px) { .app-main { padding: 12px; } }
</style>
......@@ -39,12 +39,12 @@ watchEffect(() => {
<template>
<el-dialog title="批量导入考试成绩" :close-on-click-modal="false" width="400px">
<el-form label-width="70px">
<el-form label-width="70px" class="import-form">
<el-form-item label="赛项名称">
<el-select v-model="form.competition_id">
<el-select v-model="form.competition_id" class="import-form__select">
<el-option v-for="item in competitions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<a :href="templateUrl" download style="margin-left: 10px" v-if="templateUrl">下载模板</a>
<a :href="templateUrl" download class="import-form__link" v-if="templateUrl">下载模板</a>
</el-form-item>
<el-form-item>
<el-button
......@@ -53,10 +53,24 @@ watchEffect(() => {
:icon="Upload"
@click="handleImport"
:disabled="!form.competition_id"
style="width: 195px"
style="width: 100%; max-width: 195px"
>本地上传</el-button
>
</el-form-item>
</el-form>
</el-dialog>
</template>
<style scoped>
@media (max-width: 767px) {
.import-form :deep(.el-form-item__content) {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.import-form__select {
width: 100%;
}
}
</style>
......@@ -39,12 +39,12 @@ watchEffect(() => {
<template>
<el-dialog title="批量导入完整评分" :close-on-click-modal="false" width="400px">
<el-form label-width="70px">
<el-form label-width="70px" class="import-form">
<el-form-item label="赛项名称">
<el-select v-model="form.competition_id">
<el-select v-model="form.competition_id" class="import-form__select">
<el-option v-for="item in competitions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<a :href="templateUrl" download style="margin-left: 10px" v-if="templateUrl">下载模板</a>
<a :href="templateUrl" download class="import-form__link" v-if="templateUrl">下载模板</a>
</el-form-item>
<el-form-item>
<el-button
......@@ -53,10 +53,24 @@ watchEffect(() => {
:icon="Upload"
@click="handleImport"
:disabled="!form.competition_id"
style="width: 195px"
style="width: 100%; max-width: 195px"
>本地上传</el-button
>
</el-form-item>
</el-form>
</el-dialog>
</template>
<style scoped>
@media (max-width: 767px) {
.import-form :deep(.el-form-item__content) {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.import-form__select {
width: 100%;
}
}
</style>
......@@ -26,15 +26,15 @@ const getUrl = (url: string) => {
<template>
<el-dialog title="查看报告" width="500px">
<ul>
<li v-for="item in reportList" :key="item.id" style="line-height: 24px">
<ul class="report-list">
<li v-for="item in reportList" :key="item.id" class="report-list__item">
<div class="report-list__name">{{ item.name }}</div>
<div class="report-list__actions">
<a :href="getUrl(item.url)" target="_blank">
{{ item.name }}
<el-button size="small" type="primary" style="margin-left: 10px">查看</el-button>
<el-button size="small" type="primary">查看</el-button>
</a>
<el-button size="small" type="primary" style="margin-left: 10px" @click="saveAs(item.url, item.name)"
>下载</el-button
>
<el-button size="small" type="primary" @click="saveAs(item.url, item.name)">下载</el-button>
</div>
</li>
</ul>
<template #footer>
......@@ -44,3 +44,40 @@ const getUrl = (url: string) => {
</template>
</el-dialog>
</template>
<style scoped>
.report-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.report-list__item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
border: 1px solid #ebeef5;
border-radius: 10px;
}
.report-list__name {
min-width: 0;
line-height: 1.6;
word-break: break-word;
}
.report-list__actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
@media (max-width: 767px) {
.report-list__item {
flex-direction: column;
align-items: stretch;
}
.report-list__actions {
justify-content: flex-end;
flex-wrap: wrap;
}
}
</style>
......@@ -2,10 +2,12 @@
import { ElMessage } from 'element-plus'
import { ArrowDown } from '@element-plus/icons-vue'
import { usePrecision } from '@vueuse/math'
import { useMediaQuery } from '@vueuse/core'
import type { ReportItem } from '../types'
import { submitScore } from '../api'
defineProps<{ reportList: ReportItem[] }>()
const isMobile = useMediaQuery('(max-width: 767px)')
const emit = defineEmits<{
(e: 'update'): void
......@@ -76,7 +78,7 @@ const handleScoreRule = function () {
<el-form-item label="赛项名称">{{ detail.competition_id_name }}</el-form-item>
<el-form-item label="选手姓名">{{ detail.student_name }}</el-form-item>
<el-form-item label="选手ID">{{ detail.login_id }}</el-form-item>
<div style="display: flex; justify-content: space-between">
<div class="score-dialog-toolbar">
<el-form-item label="报告" v-if="reportList.length">
<el-dropdown>
<el-button text>
......@@ -94,7 +96,31 @@ const handleScoreRule = function () {
<el-button type="primary" style="margin-bottom: 20px" @click="handleScoreRule">查看评分规则</el-button>
</div>
</el-form>
<el-table :data="tableList" :header-cell-style="{ background: '#ededed' }">
<div v-if="isMobile" class="score-card-list">
<div class="score-card" v-for="row in tableList" :key="row.id">
<div class="score-card__row"><span>评分规则</span><strong>{{ row.name || '-' }}</strong></div>
<div class="score-card__row"><span>评分方法</span><strong>{{ row.type_name || '-' }}</strong></div>
<div class="score-card__row"><span>分值</span><strong>{{ row.old_score || '-' }}</strong></div>
<div class="score-card__row"><span>占比(%)</span><strong>{{ row.ratio || '-' }}</strong></div>
<div class="score-card__row score-card__row--field">
<span>评分</span>
<div class="score-card__field">
<el-input-number
:disabled="row.type === '2'"
:controls="false"
v-model="row.score"
step-strictly
:step="0.1"
:min="0"
:max="row.old_score"
style="width: 100%"
placeholder="--"
/>
</div>
</div>
</div>
</div>
<el-table v-else :data="tableList" :header-cell-style="{ background: '#ededed' }">
<el-table-column label="评分规则" prop="name" align="center"></el-table-column>
<el-table-column label="评分方法" prop="type_name" align="center"></el-table-column>
<el-table-column label="分值" prop="old_score" align="center"></el-table-column>
......@@ -146,4 +172,58 @@ const handleScoreRule = function () {
color: var(--main-color);
}
}
.score-dialog-toolbar {
display: flex;
justify-content: space-between;
gap: 12px;
}
.score-card-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.score-card {
padding: 14px 16px;
border: 1px solid #ebeef5;
border-radius: 10px;
background: #fff;
}
.score-card__row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 0;
border-bottom: 1px solid #f3f4f6;
}
.score-card__row:last-of-type {
border-bottom: none;
}
.score-card__row span {
flex-shrink: 0;
color: #909399;
}
.score-card__row strong {
min-width: 0;
text-align: right;
word-break: break-word;
}
.score-card__row--field {
align-items: center;
}
.score-card__field {
width: 120px;
max-width: 100%;
}
@media (max-width: 767px) {
.score-dialog-toolbar {
flex-direction: column;
align-items: flex-start;
}
.result {
padding: 24px 0;
span {
font-size: 30px;
}
}
}
</style>
<script setup lang="ts">
import type { RecordItem, FileItem } from '../types'
import { useMediaQuery } from '@vueuse/core'
interface Props {
data: RecordItem
}
const props = defineProps<Props>()
const isMobile = useMediaQuery('(max-width: 767px)')
// 实验过程截图
const pictures = $computed<FileItem[]>(() => {
......@@ -24,7 +26,7 @@ const srcList = computed(() => {
<template>
<el-dialog title="查看实验截图" width="800px" append-to-body>
<ul class="picture-list">
<ul class="picture-list" :class="{ 'picture-list--mobile': isMobile }">
<li v-for="(item, index) in pictures" :key="item.url">
<p class="t1">
<el-image style="width: 100%" :src="item.url" :preview-src-list="srcList" :initial-index="index" />
......@@ -45,6 +47,7 @@ const srcList = computed(() => {
width: 100%;
li {
line-height: 30px;
margin-bottom: 16px;
}
a {
color: var(--main-color);
......@@ -53,4 +56,17 @@ const srcList = computed(() => {
text-align: center;
}
}
.picture-list--mobile {
li {
padding: 12px;
border: 1px solid #ebeef5;
border-radius: 10px;
background: #fff;
}
.t2 {
margin-top: 8px;
font-size: 13px;
color: #909399;
}
}
</style>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { useMediaQuery } from '@vueuse/core'
import { useMapStore } from '@/stores/map'
import { useFilterList } from '../composables/useFilterList'
import { syncExam } from '../api'
......@@ -10,6 +11,7 @@ const emit = defineEmits<{
// 评分方法
const scoreMethods = useMapStore().getMapValuesByKey('scoring_manner')
const isMobile = useMediaQuery('(max-width: 767px)')
const { competitions } = useFilterList(1)
const form = reactive<any>({
......@@ -30,16 +32,25 @@ function scoreMethodText(value: string) {
<template>
<el-dialog title="同步1+同步线上考试成绩" :close-on-click-modal="false">
<el-form>
<el-form class="sync-form">
<el-form-item label="赛项名称">
<el-select v-model="form.competition" value-key="id">
<el-select v-model="form.competition" value-key="id" class="sync-form__select">
<el-option v-for="item in competitions" :key="item.id" :label="item.name" :value="item"></el-option>
</el-select>
&nbsp;&nbsp;
<el-button type="primary" :disabled="!form.competition" @click="handleSync">一键同步成绩</el-button>
</el-form-item>
</el-form>
<el-table :data="form.competition?.rules">
<div v-if="isMobile" class="sync-card-list">
<div class="sync-card" v-for="row in form.competition?.rules || []" :key="row.id || row.exam_name">
<div class="sync-card__row"><span>评分方法</span><strong>{{ scoreMethodText(row.type) }}</strong></div>
<div class="sync-card__row"><span>考试名称</span><strong>{{ row.exam_name || '-' }}</strong></div>
<div class="sync-card__row"><span>实考人数/应考人数</span><strong>{{ row.finish_count }}/{{ row.all_count }}</strong></div>
<div class="sync-card__actions">
<el-button round type="primary" @click="handleSync(row)">同步导入成绩</el-button>
</div>
</div>
</div>
<el-table v-else :data="form.competition?.rules">
<el-table-column label="评分方法" prop="type" align="center" width="200">
<template #default="{ row }"> {{ scoreMethodText(row.type) }} </template>
</el-table-column>
......@@ -55,3 +66,52 @@ function scoreMethodText(value: string) {
</el-table>
</el-dialog>
</template>
<style scoped>
.sync-card-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.sync-card {
padding: 14px 16px;
border: 1px solid #ebeef5;
border-radius: 10px;
background: #fff;
}
.sync-card__row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 0;
border-bottom: 1px solid #f3f4f6;
}
.sync-card__row:last-of-type {
border-bottom: none;
}
.sync-card__row span {
flex-shrink: 0;
color: #909399;
}
.sync-card__row strong {
min-width: 0;
text-align: right;
word-break: break-word;
}
.sync-card__actions {
display: flex;
justify-content: flex-end;
padding-top: 10px;
}
@media (max-width: 767px) {
.sync-form :deep(.el-form-item__content) {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.sync-form__select {
width: 100%;
}
}
</style>
......@@ -55,6 +55,7 @@ const listOptions = $computed(() => {
},
{ label: '操作', slots: 'table-x', width: 130 },
],
mobileCards: true,
}
})
const handleViewLive = function (row: any) {
......
......@@ -75,7 +75,7 @@ function handleResize() {
</template>
<template #right v-if="detail">
<AppCard>
<el-row justify="space-between">
<el-row justify="space-between" class="detail-toolbar">
<div class="info">
<p>
赛项名称:<span>{{ detail.competition_id_name }}</span>
......@@ -87,11 +87,11 @@ function handleResize() {
选手ID:<span>{{ detail.login_id }}</span>
</p>
</div>
<div>
<el-select v-model="platformUrl">
<div class="detail-toolbar__actions">
<el-select v-model="platformUrl" class="detail-toolbar__select">
<el-option v-for="item in platformList" :key="item.id" :label="item.name" :value="item.url"></el-option>
</el-select>
<el-dropdown style="margin: 0 10px">
<el-dropdown class="detail-toolbar__dropdown">
<el-button plain>
查看报告<el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
......@@ -169,4 +169,33 @@ function handleResize() {
margin-right: 40px;
}
}
.detail-toolbar__actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
@media (max-width: 767px) {
.detail-toolbar {
gap: 12px;
}
.info {
width: 100%;
p {
margin-right: 0;
width: 100%;
line-height: 1.6;
word-break: break-word;
}
}
.detail-toolbar__actions {
width: 100%;
}
.detail-toolbar__select {
width: 100%;
}
.detail-toolbar__dropdown {
margin: 0 !important;
}
}
</style>
......@@ -50,7 +50,7 @@ watchEffect(() => {
align-items: center;
justify-content: center;
.el-button {
width: 220px;
max-width: 220px; width: 100%;
}
p {
color: #999;
......
......@@ -234,11 +234,11 @@ function getModuleStatus(row: any, index: number) {
</li>
</ul>
<el-row :gutter="20">
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<h2 class="h2-title">已提交选手</h2>
<AppList border v-bind="submittedListOptions" :data="submittedLis" ref="appList"></AppList
></el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<h2 class="h2-title">正在作答选手</h2>
<AppList border v-bind="answeringListOptions" :data="answeringList" ref="appList"></AppList
></el-col>
......@@ -255,8 +255,9 @@ function getModuleStatus(row: any, index: number) {
justify-content: center;
column-gap: 140px;
margin: 90px 0;
flex-wrap: wrap; row-gap: 20px;
li {
width: 212px;
flex: 0 0 212px;
height: 212px;
background-color: rgba(247, 247, 247, 1);
border-radius: 50%;
......@@ -353,4 +354,5 @@ function getModuleStatus(row: any, index: number) {
font-size: 30px;
}
}
@media (max-width: 767px) { .statistics { justify-content: center; margin: 40px 0; } .statistics li { flex: 0 0 140px; height: 140px; } }
</style>
......@@ -53,6 +53,8 @@ defineEmits<{
.discuss-box-user {
display: flex;
align-items: center;
flex-wrap: wrap;
min-width: 0;
.button-comment {
display: flex;
align-items: center;
......@@ -112,6 +114,7 @@ defineEmits<{
font-weight: 400;
line-height: 24px;
color: #333333;
word-break: break-word;
}
.discuss-comment {
......@@ -134,12 +137,16 @@ defineEmits<{
margin-left: 10px;
color: #333;
overflow: hidden;
word-break: break-word;
}
.discuss-comment__username {
font-size: 16px;
line-height: 20px;
color: var(--main-color);
}
@media (max-width: 767px) {
.discuss-item { padding: 12px 8px; }
}
.discuss-comment-time {
margin-top: 10px;
font-size: 14px;
......
......@@ -48,7 +48,7 @@ watchEffect(() => {
align-items: center;
justify-content: center;
.el-button {
width: 220px;
max-width: 220px; width: 100%;
}
p {
color: #999;
......
......@@ -435,8 +435,8 @@ onMounted(() => {
>
<div style="display: flex; align-items: center">
<!-- <span style="margin-right: 10px; width: 180px">{{ item.name }}</span> -->
<el-input v-model="item.name" style="margin-right: 10px; max-width: 130px" />
<el-input v-model="item.url" style="width: 200px" />
<el-input v-model="item.name" style="margin-right: 10px; max-width: 130px; width: 100%" />
<el-input v-model="item.url" style="max-width: 200px; width: 100%" />
</div>
</el-checkbox>
</el-form-item>
......@@ -451,9 +451,9 @@ onMounted(() => {
>
<div style="display: flex; align-items: center">
<!-- <span style="margin-right: 10px; width: 180px">{{ item.name }}</span> -->
<el-input v-model="item.name" style="margin-right: 10px; max-width: 130px" />
<el-input v-model="item.url" v-if="item.type === '2'" style="width: 200px" />
<el-select v-model="item.exam_id" filterable style="width: 200px" v-if="item.type === '1'">
<el-input v-model="item.name" style="margin-right: 10px; max-width: 130px; width: 100%" />
<el-input v-model="item.url" v-if="item.type === '2'" style="max-width: 200px; width: 100%" />
<el-select v-model="item.exam_id" filterable style="max-width: 200px; width: 100%" v-if="item.type === '1'">
<el-option
v-for="item in examList"
:key="item.exam_id"
......
......@@ -432,8 +432,8 @@ onMounted(() => {
>
<div style="display: flex; align-items: center">
<!-- <span style="margin-right: 10px; width: 180px">{{ item.name }}</span> -->
<el-input v-model="item.name" style="margin-right: 10px; max-width: 130px" />
<el-input v-model="item.url" style="width: 200px" />
<el-input v-model="item.name" style="margin-right: 10px; max-width: 130px; width: 100%" />
<el-input v-model="item.url" style="max-width: 200px; width: 100%" />
</div>
</el-checkbox>
</el-form-item>
......@@ -514,9 +514,9 @@ onMounted(() => {
>
<div style="display: flex; align-items: center">
<!-- <span style="margin-right: 10px; width: 180px">{{ item.name }}</span> -->
<el-input v-model="item.name" style="margin-right: 10px; max-width: 130px" />
<el-input v-model="item.url" v-if="item.type === '2'" style="width: 200px" />
<el-select v-model="item.exam_id" filterable style="width: 200px" v-if="item.type === '1'">
<el-input v-model="item.name" style="margin-right: 10px; max-width: 130px; width: 100%" />
<el-input v-model="item.url" v-if="item.type === '2'" style="max-width: 200px; width: 100%" />
<el-select v-model="item.exam_id" filterable style="max-width: 200px; width: 100%" v-if="item.type === '1'">
<el-option
v-for="item in examList"
:key="item.exam_id"
......
......@@ -63,7 +63,7 @@ function handleSubmit() {
remote-show-suffix
:remote-method="fetchExamList"
:loading="loading"
style="width: 340px"
style="width: 100%; max-width: 340px"
>
<el-option v-for="item in list" :key="item.value" :label="item.name" :value="item.id" />
</el-select>
......
......@@ -41,7 +41,8 @@ onMounted(() => {
}
}
.top-cover {
width: 300px;
max-width: 300px;
width: 100%;
margin-right: 20px;
p {
font-weight: normal;
......
......@@ -213,7 +213,8 @@ function handleExperts() {
}
}
.top-cover {
width: 300px;
max-width: 300px;
width: 100%;
margin-right: 20px;
p {
font-weight: normal;
......
......@@ -32,9 +32,9 @@ function handleSubmit() {
</p>
<br />
<p>
<span>报名人数:{{ data.all_competitor_count }}</span>
<span>完赛人数:{{ data.complete_competitor_count }}</span>
<span>已评分人数:{{ data.checked_competitor_count }}</span>
<span class="content__metric">报名人数:{{ data.all_competitor_count }}</span>
<span class="content__metric">完赛人数:{{ data.complete_competitor_count }}</span>
<span class="content__metric">已评分人数:{{ data.checked_competitor_count }}</span>
</p>
<br />
<p>请确认是否发布成绩!</p>
......@@ -55,8 +55,20 @@ function handleSubmit() {
b {
color: var(--main-color);
}
span + span {
}
.content__metric {
display: inline-block;
}
.content__metric + .content__metric {
margin-left: 40px;
}
@media (max-width: 767px) {
.content__metric {
display: block;
margin: 6px 0;
}
.content__metric + .content__metric {
margin-left: 0;
}
}
</style>
......@@ -2,10 +2,6 @@ import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/admin/lab',
redirect: '/admin/lab/book'
},
{
path: '/admin/lab/book',
component: AppLayout,
......
......@@ -85,4 +85,6 @@ function onUpdateSuccess() {
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
@media (max-width: 1024px) { .list-card { grid-template-columns: repeat(2, 1fr); gap: 16px; } }
@media (max-width: 767px) { .list-card { grid-template-columns: 1fr; gap: 12px; } }
</style>
......@@ -288,10 +288,10 @@ async function handleBatchAuthorize() {
<template>
<AppCard title="案例授权">
<div class="case-auth-toolbar">
<el-select v-model="selectedCourse" placeholder="请选择课程" filterable clearable style="width: 200px">
<el-select v-model="selectedCourse" placeholder="请选择课程" filterable clearable style="width: 100%; max-width: 200px">
<el-option v-for="item in courses" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-select v-model="selectedExperiment" placeholder="请选择实验" filterable clearable style="width: 200px">
<el-select v-model="selectedExperiment" placeholder="请选择实验" filterable clearable style="width: 100%; max-width: 200px">
<el-option v-for="item in experiments" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-button type="primary" :loading="submitting" @click="handleBatchAuthorize" :disabled="!selectedCount"
......
......@@ -2,10 +2,6 @@ import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/admin/lab',
redirect: '/admin/lab/case'
},
{
path: '/admin/lab/case',
component: AppLayout,
......
......@@ -85,4 +85,6 @@ function onUpdateSuccess() {
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
@media (max-width: 1024px) { .list-card { grid-template-columns: repeat(2, 1fr); gap: 16px; } }
@media (max-width: 767px) { .list-card { grid-template-columns: 1fr; gap: 12px; } }
</style>
......@@ -104,11 +104,10 @@ const listOptions = computed(() => {
align-items: center;
justify-content: space-evenly;
margin: 90px 0;
&:empty {
display: none;
}
flex-wrap: wrap; row-gap: 20px;
&:empty { display: none; }
li {
width: 212px;
flex: 0 0 212px;
height: 212px;
background-color: rgba(247, 247, 247, 1);
border-radius: 50%;
......@@ -147,4 +146,5 @@ const listOptions = computed(() => {
margin: 20px 0;
border-left: 3px solid #aa1941;
}
@media (max-width: 767px) { .statistics { margin: 40px 0; } .statistics li { flex: 0 0 140px; height: 140px; } }
</style>
......@@ -107,11 +107,10 @@ const listOptions = computed(() => {
align-items: center;
justify-content: space-evenly;
margin: 90px 0;
&:empty {
display: none;
}
flex-wrap: wrap; row-gap: 20px;
&:empty { display: none; }
li {
width: 212px;
flex: 0 0 212px;
height: 212px;
background-color: rgba(247, 247, 247, 1);
border-radius: 50%;
......@@ -150,4 +149,5 @@ const listOptions = computed(() => {
margin: 20px 0;
border-left: 3px solid #aa1941;
}
@media (max-width: 767px) { .statistics { margin: 40px 0; } .statistics li { flex: 0 0 140px; height: 140px; } }
</style>
......@@ -53,6 +53,8 @@ defineEmits<{
.discuss-box-user {
display: flex;
align-items: center;
flex-wrap: wrap;
min-width: 0;
.button-comment {
display: flex;
align-items: center;
......@@ -112,6 +114,7 @@ defineEmits<{
font-weight: 400;
line-height: 24px;
color: #333333;
word-break: break-word;
}
.discuss-comment {
......@@ -134,12 +137,16 @@ defineEmits<{
margin-left: 10px;
color: #333;
overflow: hidden;
word-break: break-word;
}
.discuss-comment__username {
font-size: 16px;
line-height: 20px;
color: var(--main-color);
}
@media (max-width: 767px) {
.discuss-item { padding: 12px 8px; }
}
.discuss-comment-time {
margin-top: 10px;
font-size: 14px;
......
......@@ -2,10 +2,6 @@ import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/admin/lab',
redirect: '/admin/lab/example',
},
{
path: '/admin/lab/example',
component: AppLayout,
......
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { useMediaQuery } from '@vueuse/core'
import { getCompetitionRule, saveCompetitionRule } from '../api'
interface Props {
id: string
}
const props = defineProps<Props>()
const isMobile = useMediaQuery('(max-width: 767px)')
const dialogVisible = ref(false)
......@@ -226,7 +228,42 @@ function disabledMinSubmitDate(time: Date) {
<p>成绩规则:</p>
<el-button type="primary" :icon="Plus" @click="handleAdd"></el-button>
</el-row>
<el-table :data="form.config.rules">
<div v-if="isMobile" class="rule-card-list">
<div class="rule-card" v-for="(row, $index) in form.config.rules" :key="`${row.type}-${$index}`">
<div class="rule-card__field">
<span class="rule-card__label">评分项</span>
<el-select v-model="row.type" style="width: 100%" @change="handleTypeChange(row)">
<el-option v-for="item in currentRuleNames(row.type)" :key="item.value" v-bind="item"></el-option>
</el-select>
</div>
<div class="rule-card__field">
<span class="rule-card__label">分数</span>
<div class="rule-card__inline">
<el-input-number v-model="row.percent" :min="0" :max="100" @change="handlePercentChange(row, $index)" />
分
</div>
</div>
<div class="rule-card__field">
<span class="rule-card__label">评分方式</span>
<el-radio-group v-model="row.rule_mode" size="small">
<el-radio :label="1">人工评分</el-radio>
<el-radio :label="2" :disabled="[4, 5, 6].includes(row.type)">自动评分</el-radio>
</el-radio-group>
</div>
<div class="rule-card__actions">
<el-button
style="padding: 0"
text
type="primary"
@click="handleEdit(row)"
:disabled="[4, 5, 6].includes(row.type) || row.rule_mode === 1"
>编辑</el-button
>
<el-button style="padding: 0" text type="primary" @click="handleRemove($index)">删除</el-button>
</div>
</div>
</div>
<el-table v-else :data="form.config.rules">
<el-table-column prop="name" width="170">
<template #default="{ row }">
<el-select v-model="row.type" style="width: 100%" @change="handleTypeChange(row)">
......@@ -305,4 +342,48 @@ function disabledMinSubmitDate(time: Date) {
padding: 5px 10px;
}
}
.rule-card-list {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
}
.rule-card {
width: 100%;
padding: 14px 16px;
border: 1px solid #ebeef5;
border-radius: 10px;
background: #fff;
box-sizing: border-box;
}
.rule-card__field + .rule-card__field {
margin-top: 12px;
}
.rule-card__label {
display: block;
margin-bottom: 8px;
color: #909399;
font-size: 13px;
}
.rule-card__inline {
display: flex;
align-items: center;
gap: 8px;
}
.rule-card__actions {
display: flex;
justify-content: flex-end;
gap: 12px;
flex-wrap: wrap;
padding-top: 12px;
}
@media (max-width: 767px) {
.total {
text-align: left;
p {
min-width: 0;
width: 100%;
}
}
}
</style>
......@@ -3,10 +3,12 @@ import type { FormInstance } from 'element-plus'
import type { ExperimentItem } from '../types'
import { Plus, Minus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { useMediaQuery } from '@vueuse/core'
import { gradeRule, gradeRuleList } from '@/utils/dictionary'
import { getExperimentGradeRule, updateExperimentGradeRule, getScoreExamList } from '../api'
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
const isMobile = useMediaQuery('(max-width: 767px)')
interface Props {
data: ExperimentItem
......@@ -269,7 +271,7 @@ onMounted(() => {
<el-form ref="formRef" :model="form" label-suffix=":">
<el-form-item label="实验名称">{{ data?.name }}</el-form-item>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="是否允许查看成绩明细" prop="is_show">
<el-radio-group v-model="form.is_show">
<el-radio label="1">是</el-radio>
......@@ -277,7 +279,7 @@ onMounted(() => {
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验总成绩">{{ data?.score }}</el-form-item>
</el-col>
</el-row>
......@@ -289,7 +291,36 @@ onMounted(() => {
<el-button type="primary" :icon="Plus" @click="handleAddExamRule" :disabled="form.exam_rules.length >= 1">
</el-button>
</el-row>
<el-table :data="form.exam_rules" row-key="id">
<div v-if="isMobile" class="rule-card-list">
<div class="rule-card" v-for="(row, $index) in form.exam_rules" :key="row.id || $index">
<div class="rule-card__field">
<span class="rule-card__label">理论考试</span>
<el-select v-model="row.exam_id" style="width: 100%">
<el-option v-for="item in examList" :key="item.id" :label="item.name" :value="item.exam_id"></el-option>
</el-select>
</div>
<div class="rule-card__field">
<span class="rule-card__label">权重</span>
<div class="rule-card__inline">
<el-input-number v-model="row.percent" :min="0" @change="handlePercentChange(row, $index, 2)" /> %
</div>
</div>
<div class="rule-card__field">
<span class="rule-card__label">评分方式</span>
<el-radio-group v-model="row.is_auto_scoring" size="small">
<el-radio label="1">自动评分</el-radio>
</el-radio-group>
</div>
<div class="rule-card__row">
<span>满分</span>
<strong>{{ rowScore(row.percent) }}</strong>
</div>
<div class="rule-card__actions">
<el-button style="padding: 0" text type="primary" @click="handleRemoveExamRule($index)" v-if="row.type !== 1">删除</el-button>
</div>
</div>
</div>
<el-table v-else :data="form.exam_rules" row-key="id">
<el-table-column prop="name" width="170">
<template #default="{ row }">
<el-select v-model="row.exam_id" style="width: 100%">
......@@ -332,7 +363,47 @@ onMounted(() => {
<p>实操成绩规则:</p>
<el-button type="primary" :icon="Plus" @click="handleAdd"></el-button>
</el-row>
<el-table :data="form.rule_list" row-key="id">
<div v-if="isMobile" class="rule-card-list">
<div class="rule-card" v-for="(row, $index) in form.rule_list" :key="row.id || `${row.type}-${$index}`">
<div class="rule-card__field">
<span class="rule-card__label">规则名称</span>
<el-input v-model="row.name" :maxlength="20" style="width: 100%" v-if="row.type === 5" />
<el-select v-model="row.type" style="width: 100%" @change="handleTypeChange(row)" v-else>
<el-option v-for="item in currentRuleNames(row.type)" :key="item.value" v-bind="item"></el-option>
</el-select>
</div>
<div class="rule-card__field">
<span class="rule-card__label">权重</span>
<div class="rule-card__inline">
<el-input-number v-model="row.percent" :min="0" @change="handlePercentChange(row, $index, 1)" /> %
</div>
</div>
<div class="rule-card__field">
<span class="rule-card__label">评分方式</span>
<el-radio-group v-model="row.rule_mode" size="small">
<el-radio :label="1">人工评分</el-radio>
<el-radio :label="2" :disabled="row.type === 1 || row.type === 5 || row.type === 8">自动评分</el-radio>
</el-radio-group>
</div>
<div class="rule-card__row">
<span>满分</span>
<strong>{{ rowScore(row.percent) }}</strong>
</div>
<div class="rule-card__actions">
<el-button
:disabled="row.type === 1"
style="padding: 0"
text
type="primary"
@click="handleEdit(row)"
v-if="row.type !== 1 && row.type !== 5"
>编辑</el-button
>
<el-button style="padding: 0" text type="primary" @click="handleRemove($index)">删除</el-button>
</div>
</div>
</div>
<el-table v-else :data="form.rule_list" row-key="id">
<el-table-column prop="name" width="170">
<template #default="{ row }">
<el-input v-model="row.name" :maxlength="20" style="width: 100%" v-if="row.type === 5" />
......@@ -400,6 +471,7 @@ onMounted(() => {
<template v-if="marketingShow">
<el-form-item
style="margin-bottom: 10px"
class="marketing-item"
label-width="150px"
:label="marketingType[item.type as '1']"
v-for="(item, index) in marketingForm.details"
......@@ -452,4 +524,64 @@ onMounted(() => {
float: right;
}
}
.rule-card-list {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
}
.rule-card {
width: 100%;
padding: 14px 16px;
border: 1px solid #ebeef5;
border-radius: 10px;
background: #fff;
box-sizing: border-box;
}
.rule-card__field + .rule-card__field {
margin-top: 12px;
}
.rule-card__label {
display: block;
margin-bottom: 8px;
color: #909399;
font-size: 13px;
}
.rule-card__inline {
display: flex;
align-items: center;
gap: 8px;
}
.rule-card__row {
display: flex;
justify-content: space-between;
gap: 12px;
padding-top: 12px;
margin-top: 12px;
border-top: 1px solid #f3f4f6;
}
.rule-card__actions {
display: flex;
justify-content: flex-end;
gap: 12px;
flex-wrap: wrap;
padding-top: 12px;
}
@media (max-width: 767px) {
.total,
.total-c {
padding: 10px 0;
p {
margin-left: 0;
min-width: 0;
width: 100%;
float: none;
}
}
.marketing-item {
:deep(.el-form-item__content) {
width: 100%;
}
}
}
</style>
......@@ -94,7 +94,8 @@ defineExpose({ formRef })
.upload-box {
display: flex;
.upload-loading {
width: 300px;
max-width: 300px;
width: 100%;
display: flex;
align-items: center;
&:hover {
......
......@@ -61,7 +61,8 @@ defineExpose({ formRef })
.upload-box {
display: flex;
.upload-loading {
width: 300px;
max-width: 300px;
width: 100%;
display: flex;
align-items: center;
&:hover {
......
......@@ -119,7 +119,8 @@ defineExpose({ formRef })
.upload-box {
display: flex;
.upload-loading {
width: 300px;
max-width: 300px;
width: 100%;
display: flex;
align-items: center;
&:hover {
......
......@@ -93,7 +93,8 @@ defineExpose({ formRef })
.upload-box {
display: flex;
.upload-loading {
width: 300px;
max-width: 300px;
width: 100%;
display: flex;
align-items: center;
&:hover {
......
......@@ -43,7 +43,7 @@ function handleHasScoreChange(value?: number) {
<template>
<div class="report-design-item">
<el-row :gutter="40">
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item
label="附件组件名称"
label-width="130px"
......@@ -52,12 +52,12 @@ function handleHasScoreChange(value?: number) {
<el-input v-model="form.name" placeholder="例:实验源代码程序包" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item label="分值" prop="score">
<el-input-number :min="0" v-model="form.score" @change="handleScoreChange" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item label="评分规则" prop="rule_mode">
<el-radio-group v-model="form.rule_mode">
<el-radio :label="1">人工评分</el-radio>
......@@ -65,7 +65,7 @@ function handleHasScoreChange(value?: number) {
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item label="最低分" prop="min_score">
<el-input-number :min="0" :max="form.score" v-model="form.min_score" v-if="form.rule_mode === 1" />
<template v-else>
......
......@@ -27,7 +27,7 @@ function handleScoreChange(value?: number) {
<template>
<div class="report-design-item">
<el-row :gutter="40">
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item
label="内容组件名称"
label-width="130px"
......@@ -36,12 +36,12 @@ function handleScoreChange(value?: number) {
<el-input v-model="form.name" placeholder="例:实验目的" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item label="分值" prop="score">
<el-input-number :min="0" v-model="form.score" @change="handleScoreChange" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item label="评分规则" prop="rule_mode">
<el-radio-group v-model="form.rule_mode">
<el-radio :label="1">人工评分</el-radio>
......@@ -49,7 +49,7 @@ function handleScoreChange(value?: number) {
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item label="最低分" prop="min_score">
<el-input-number :min="0" :max="form.score" v-model="form.min_score" />
</el-form-item>
......
......@@ -17,17 +17,17 @@ watchEffect(() => {
<div class="report-design-item">
<el-form :model="form" label-suffix=":">
<el-row :gutter="40">
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item label="试卷组件名称" prop="name" label-width="130px">
<el-input v-model="form.name" placeholder="例:商业数据分析基础理论" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item label="分值" prop="score">
<el-input-number :min="0" v-model="form.score" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item label="评分规则" prop="rule_mode">
<el-radio-group v-model="form.rule_mode">
<el-radio :label="1">人工评分</el-radio>
......@@ -35,7 +35,7 @@ watchEffect(() => {
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item label="最低分" prop="min_score">
<el-input-number :min="0" v-model="form.min_score" />
</el-form-item>
......
......@@ -27,7 +27,7 @@ function handleScoreChange(value?: number) {
<template>
<div class="report-design-item">
<el-row :gutter="40">
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item
label="思考题组件名称"
label-width="130px"
......@@ -36,12 +36,12 @@ function handleScoreChange(value?: number) {
<el-input v-model="form.name" placeholder="例:实验扩展思考" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item label="分值" prop="score">
<el-input-number :min="0" v-model="form.score" @change="handleScoreChange" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item label="评分规则" prop="rule_mode">
<el-radio-group v-model="form.rule_mode">
<el-radio :label="1">人工评分</el-radio>
......@@ -49,7 +49,7 @@ function handleScoreChange(value?: number) {
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :xs="12" :sm="12" :md="6">
<el-form-item label="最低分" prop="min_score">
<el-input-number :min="0" :max="form.score" v-model="form.min_score" />
</el-form-item>
......
......@@ -16,42 +16,42 @@ const teacherText = $computed(() => {
<h1>实验报告</h1>
<el-form label-suffix=":">
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="课程名称">{{ data.course_name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验名称">{{ data.name }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验地点"></el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验日期"></el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验类型">{{ data.type_name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="指导教师">{{ teacherText }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="专业"></el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="班级"></el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="姓名"></el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="学号"></el-form-item>
</el-col>
</el-row>
......
......@@ -143,6 +143,7 @@ const handleSubmit = async () => {
<style lang="scss" scoped>
.btn-box {
display: flex;
flex-wrap: wrap; gap: 8px;
margin-bottom: 20px;
.btn-r {
margin-left: auto;
......
......@@ -107,21 +107,21 @@ function handlePreview() {
<AppCard title="实验报告规则设置">
<el-form ref="formRef" :model="form" label-suffix=":" hide-required-asterisk v-if="detail">
<el-row>
<el-col :span="8">
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="实验名称">{{ detail.name }}</el-form-item>
</el-col>
<el-col :span="8">
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="实验课程名称">{{ detail.course_name }}</el-form-item>
</el-col>
<el-col :span="8">
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="所属机构/学校">{{ detail.organ_id_name }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="实验类型">{{ detail.type_name }}</el-form-item>
</el-col>
<el-col :span="8">
<el-col :xs="24" :sm="12" :md="8">
<el-form-item :span="3" label="指导教师">{{ teacherText }}</el-form-item>
</el-col>
</el-row>
......
......@@ -231,3 +231,16 @@ function handleUpdateGradeRules() {
:data="detail"
v-if="gradeRulesDialogVisible && detail"></GradeRulesDialog>
</template>
<style lang="scss" scoped>
.btn-all {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
@media (max-width: 767px) {
.btn-all .el-button {
width: 100%;
}
}
</style>
......@@ -50,7 +50,7 @@ watchEffect(() => {
align-items: center;
justify-content: center;
.el-button {
width: 220px;
max-width: 220px; width: 100%;
}
p {
color: #999;
......
......@@ -148,7 +148,7 @@ const handleSubmit = function () {
<el-button type="primary" @click="updateScore(questionDetail)">保存</el-button>
</el-row>
</el-card>
<el-row justify="center">
<el-row justify="center" class="question-actions">
<el-button :disabled="!hasPrev" type="primary" @click="handlePrev">上一题</el-button>
<el-button :disabled="!hasNext" type="primary" @click="handleNext">下一题</el-button>
<el-button type="primary" @click="handleSubmit()">提交</el-button>
......@@ -237,4 +237,51 @@ const handleSubmit = function () {
font-size: 14px;
}
}
.question-actions {
gap: 8px;
}
@media (max-width: 767px) {
.question-number {
dd {
display: flex;
flex-wrap: wrap;
gap: 8px;
span {
margin-right: 0;
margin-bottom: 0;
}
}
}
.question-item__stem,
.question-item__content,
.question-item__tip {
line-height: 1.6;
word-break: break-word;
}
.question-item__files-item {
align-items: flex-start;
}
.question-item__files-item__title {
min-width: 0;
p {
word-break: break-word;
}
}
.question-item-score {
flex-direction: column;
gap: 10px;
span {
margin-right: 0;
}
}
.question-actions {
:deep(.el-button) {
margin-left: 0;
width: calc(50% - 4px);
}
:deep(.el-button:last-child) {
width: 100%;
}
}
}
</style>
<script setup lang="ts">
import type { RecordItem, FileItem } from '../types'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useMediaQuery } from '@vueuse/core'
import { getExperimentScore, getExperimentScoreTemplate, updateExperimentScore, getExperimentReport } from '../api'
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
const isMobile = useMediaQuery('(max-width: 767px)')
const ScoreViewPicturesDialog = defineAsyncComponent(() => import('./ScoreViewPicturesDialog.vue'))
const ScoreViewPrepareDialog = defineAsyncComponent(() => import('./ScoreViewPrepareDialog.vue'))
......@@ -171,33 +173,33 @@ function handleViewAuto(row: any) {
@update:modelValue="(value) => $emit('update:modelValue', value)">
<el-form label-width="120px" label-suffix=":" v-if="detail">
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验名称">{{ data.experiment_name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验课程名称">{{ experiment.course.name }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="学生姓名">{{ data.student_name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="学生学号">{{ data.sno_number }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="所属专业">{{ data.specialty_name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="所属班级">{{ data.class_name }}</el-form-item>
</el-col>
</el-row>
<el-divider />
<el-form-item label="理论成绩">{{ data.theory_score }}</el-form-item>
<el-form-item label="实验成绩">
<el-row justify="end" style="width: 100%">
<el-row justify="end" style="width: 100%" class="score-action-row">
<el-button type="primary" @click="prepareVisible = true">查看实验准备</el-button>
<el-button type="primary" @click="resultVisible = true">查看实验结果</el-button>
<el-button type="primary" @click="pictureVisible = true">查看截图</el-button>
......@@ -208,11 +210,60 @@ function handleViewAuto(row: any) {
<p class="t1">{{ scoreValue(score) }}</p>
<p class="t2">满分:{{ experiment.score }}</p>
</div>
<div v-if="isMobile" class="score-detail-card-list">
<div class="score-detail-card" v-for="row in form.score_details" :key="`${row.type}-${row.name}`">
<div class="score-detail-card__row"><span>实验成绩组成项</span><strong>{{ row.name || '-' }}</strong></div>
<div class="score-detail-card__row"><span>权重</span><strong>{{ row.percent }}%</strong></div>
<div class="score-detail-card__row"><span>满分</span><strong>{{ row.score || '-' }}</strong></div>
<div class="score-detail-card__row score-detail-card__row--field">
<span>得分</span>
<div class="score-detail-card__field">
<el-input-number
v-model="row.commit_score"
:min="0"
:max="100"
:controls="false"
:step="0.01"
step-strictly
placeholder="请输入成绩"
style="width: 100%"
v-if="!(row.type === 1 && experiment.report_upload_way === 2)"></el-input-number>
<strong v-else>{{ row.commit_score }}</strong>
</div>
</div>
<div class="score-detail-card__actions">
<template v-if="row.type === 1">
<el-button link type="primary" v-if="experiment.report_upload_way === 2">
<a :href="getOperationUrl(row.type)" target="_blank">批改</a>
</el-button>
<template v-if="experiment.report_upload_way === 1">
<el-button link type="primary" v-if="file?.url">
<a :href="file.url" target="_blank">查阅报告</a>
</el-button>
<p class="score-detail-card__warning" v-else>未上传</p>
</template>
</template>
<template v-if="[6, 7, 9, 11, 12].includes(row.type)">
<el-button link type="primary" @click="handleViewAuto(row)">查看自动评分结果</el-button>
</template>
<template v-if="[6, 7, 8, 9, 10, 11, 12].includes(row.type)">
<el-button link type="primary">
<a
:href="`/admin/lab/score/score?id=${props.data.experiment_id}&sid=${props.data.student_id}&type=${row.type}`"
target="_blank"
>查看原始结果</a
>
</el-button>
</template>
</div>
</div>
</div>
<el-table
:data="form.score_details"
stripe
:header-cell-style="{ background: '#ededed' }"
style="margin-top: 20px">
style="margin-top: 20px"
v-else>
<el-table-column label="实验成绩组成项" prop="name" align="left"></el-table-column>
<el-table-column label="权重" prop="percent" align="center" width="80">
<template #default="{ row }"> {{ row.percent }}% </template>
......@@ -301,6 +352,7 @@ function handleViewAuto(row: any) {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
}
.score-result__total {
min-width: 120px;
......@@ -311,4 +363,63 @@ function handleViewAuto(row: any) {
color: var(--main-color);
}
}
.score-detail-card-list {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 20px;
}
.score-detail-card {
padding: 14px 16px;
border: 1px solid #ebeef5;
border-radius: 10px;
background: #fff;
}
.score-detail-card__row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 0;
border-bottom: 1px solid #f3f4f6;
}
.score-detail-card__row:last-of-type {
border-bottom: none;
}
.score-detail-card__row span {
flex-shrink: 0;
color: #909399;
}
.score-detail-card__row strong {
min-width: 0;
text-align: right;
word-break: break-word;
}
.score-detail-card__row--field {
align-items: center;
}
.score-detail-card__field {
width: 140px;
max-width: 100%;
}
.score-detail-card__actions {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: 8px;
padding-top: 8px;
}
.score-detail-card__warning {
color: red;
}
@media (max-width: 767px) {
.score-action-row {
gap: 8px;
justify-content: flex-start !important;
}
.score-result {
flex-direction: column;
align-items: stretch;
}
}
</style>
<script setup lang="ts">
import type { RecordItem, FileItem } from '../types'
import { useMediaQuery } from '@vueuse/core'
interface Props {
data: RecordItem
}
const props = defineProps<Props>()
const isMobile = useMediaQuery('(max-width: 767px)')
// 实验过程截图
const pictures = $computed<FileItem[]>(() => {
......@@ -19,7 +21,7 @@ const pictures = $computed<FileItem[]>(() => {
<template>
<el-dialog title="查看实验截图" width="800px">
<ul class="picture-list">
<ul class="picture-list" :class="{ 'picture-list--mobile': isMobile }">
<li v-for="item in pictures" :key="item.url">
<p class="t1">
<a :href="item.url" target="_blank">{{ item.name }}</a>
......@@ -42,6 +44,7 @@ const pictures = $computed<FileItem[]>(() => {
display: flex;
justify-content: space-between;
line-height: 30px;
gap: 12px;
}
a {
color: var(--main-color);
......@@ -53,4 +56,21 @@ const pictures = $computed<FileItem[]>(() => {
overflow: hidden;
}
}
.picture-list--mobile {
li {
display: block;
padding: 12px 14px;
border: 1px solid #ebeef5;
border-radius: 10px;
margin-bottom: 12px;
line-height: 1.7;
}
.t1 {
white-space: normal;
word-break: break-word;
}
.t2 {
color: #909399;
}
}
</style>
<script setup>
import { ElMessage } from 'element-plus'
import { useMediaQuery } from '@vueuse/core'
import { getExperimentScore, getExperimentReport, updateExperimentReport, getExperimentScoreTemplate } from '../api'
import { reportScoreRule } from '@/utils/dictionary'
const route = useRoute()
const isMobile = useMediaQuery('(max-width: 767px)')
let experiment = $ref()
let report = $ref()
......@@ -122,42 +124,42 @@ function scoreValue(value) {
scroll-to-error
v-if="experiment && report">
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="课程名称">{{ experiment.course.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验名称">{{ experiment.name }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验地点">{{ report.experiment_address }} </el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验日期"> {{ report.experiment_date }} </el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验类型">{{ experiment.type_level }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="指导教师">{{ teacherText }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="专业">{{ experiment.student.specialty.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="班级">{{ classText }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="姓名">{{ experiment.student.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="学号">{{ experiment.student.sno_number }}</el-form-item>
</el-col>
</el-row>
......@@ -195,7 +197,22 @@ function scoreValue(value) {
</template>
<!-- 附件 -->
<template v-if="item.type === 2">
<el-table :data="item.files" stripe :header-cell-style="{ background: '#ededed' }" style="margin-top: 20px">
<div v-if="isMobile" class="report-file-card-list">
<div class="report-file-card" v-for="(file, fileIndex) in item.files || []" :key="file.id || file.url || fileIndex">
<div class="report-file-card__row"><span>序号</span><strong>{{ fileIndex + 1 }}</strong></div>
<div class="report-file-card__row">
<span>附件类型</span>
<strong>{{ file.type === '1' ? '本地上传' : '实验截图' }}</strong>
</div>
<div class="report-file-card__row"><span>附件名称</span><strong>{{ file.name || '-' }}</strong></div>
<div class="report-file-card__row"><span>更新时间</span><strong>{{ file.updated_time || '-' }}</strong></div>
<div class="report-file-card__actions">
<el-button text type="primary"><a :href="file.url" target="_blank">查看</a></el-button>
</div>
</div>
<el-empty v-if="!item.files?.length" description="暂无附件" />
</div>
<el-table v-else :data="item.files" stripe :header-cell-style="{ background: '#ededed' }" style="margin-top: 20px">
<el-table-column label="序号" type="index" width="80" align="center"></el-table-column>
<el-table-column label="附件类型" prop="type" align="center">
<template #default="{ row }">
......@@ -314,6 +331,42 @@ function scoreValue(value) {
justify-content: space-between;
line-height: 32px;
}
.report-file-card-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 20px;
}
.report-file-card {
padding: 14px 16px;
border: 1px solid #ebeef5;
border-radius: 10px;
background: #fff;
}
.report-file-card__row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 0;
border-bottom: 1px solid #f3f4f6;
}
.report-file-card__row:last-of-type {
border-bottom: none;
}
.report-file-card__row span {
flex-shrink: 0;
color: #909399;
}
.report-file-card__row strong {
min-width: 0;
text-align: right;
word-break: break-word;
}
.report-file-card__actions {
display: flex;
justify-content: flex-end;
padding-top: 8px;
}
.t2 {
margin: 40px 0 20px;
......@@ -323,4 +376,34 @@ function scoreValue(value) {
text-align: center;
line-height: 40px;
}
@media (max-width: 767px) {
.report-title {
padding: 12px 0 20px;
}
.report-score {
margin: 24px 0;
text-align: center;
font-size: 20px;
}
.report-form-item {
margin-bottom: 24px;
.form-hd {
flex-direction: column;
gap: 8px;
align-items: flex-start;
span {
padding: 0;
}
}
}
.report-form-item-comment_hd {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.t2 {
margin: 24px 0 16px;
line-height: 36px;
}
}
</style>
......@@ -85,4 +85,7 @@ function onUpdateSuccess() {
grid-template-columns: repeat(5, 1fr);
gap: 20px;
}
@media (max-width: 1400px) { .list-card { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 1024px) { .list-card { grid-template-columns: repeat(2, 1fr); gap: 16px; } }
@media (max-width: 767px) { .list-card { grid-template-columns: 1fr; gap: 12px; } }
</style>
......@@ -50,8 +50,8 @@ watch(messages.value, () => nextTick(() => scrollToBottom()))
:srcdoc="item.content"
frameborder="0"
v-if="item.type === 'html'"
width="900"
height="500"></iframe>
width="100%"
height="400"></iframe>
<div v-html="item.content" v-else></div>
</div>
</div>
......@@ -94,7 +94,8 @@ watch(messages.value, () => nextTick(() => scrollToBottom()))
display: flex;
flex-direction: column;
flex: 1;
width: 1000px;
max-width: 1000px;
width: 100%;
overflow: hidden;
.ai-upload {
margin: 40px 0;
......@@ -106,6 +107,7 @@ watch(messages.value, () => nextTick(() => scrollToBottom()))
}
.ai-header-inner {
max-width: 1000px;
width: 100%;
padding: 18px 0;
display: flex;
align-items: center;
......@@ -235,4 +237,8 @@ watch(messages.value, () => nextTick(() => scrollToBottom()))
background-color: #000;
}
}
@media (max-width: 767px) {
.ai-header-inner { flex-direction: column; gap: 8px; }
.ai-main { padding: 12px; }
}
</style>
......@@ -106,6 +106,8 @@ function handlePrint() {
background: url(@/assets/images/drawLots/bg.png) no-repeat center top;
.inner {
max-width: 1100px;
width: 100%;
padding: 0 16px;
margin: 0 auto;
}
h1 {
......@@ -137,7 +139,8 @@ function handlePrint() {
.box {
position: relative;
z-index: 100;
width: 800px;
max-width: 800px;
width: 100%;
height: 260px;
background: #ab0a3d;
border-radius: 8px;
......@@ -152,7 +155,8 @@ function handlePrint() {
text-align: center;
}
.box-main {
width: 760px;
max-width: 760px;
width: 100%;
height: 260px;
background: #fff url(@/assets/images/drawLots/bg3.png) no-repeat right 10px center;
border-radius: 8px;
......@@ -175,7 +179,7 @@ function handlePrint() {
}
.box-info {
position: relative;
width: 491px;
max-width: 491px; width: 100%;
height: 167px;
background: url(@/assets/images/drawLots/bg2.png) no-repeat;
display: flex;
......@@ -240,7 +244,8 @@ function handlePrint() {
position: absolute;
top: 0;
left: -20px;
width: 840px;
max-width: 840px;
width: 100%;
height: 30px;
background: #ab0a3d;
border-radius: 8px;
......@@ -272,4 +277,124 @@ function handlePrint() {
margin: 40px 0 20px;
}
}
@media (max-width: 767px) {
.drawLots {
background-size: cover;
.inner {
padding: 0 12px 24px;
}
h1 {
padding: 40px 0 24px;
font-size: 28px;
line-height: 1.4;
}
.search {
margin: 0 0 24px;
flex-wrap: wrap;
gap: 12px;
.el-input {
width: 100%;
height: 48px;
font-size: 16px;
}
.el-button {
margin-left: 0;
width: 100%;
height: 48px;
font-size: 16px;
}
}
.box {
height: auto;
.box-title {
padding: 0 12px;
font-size: 18px;
}
.box-main {
flex-direction: column;
height: auto;
padding: 20px 12px;
gap: 16px;
background-position: right bottom;
background-size: 120px auto;
}
.box-info {
width: 100%;
max-width: 100%;
height: auto;
background-size: cover;
background-position: center;
flex-direction: column;
padding: 16px 12px;
box-sizing: border-box;
.name {
position: static;
max-width: 100%;
margin-bottom: 10px;
white-space: normal;
word-break: break-word;
}
.col {
width: 100%;
margin-top: 0;
margin-right: 0;
}
dl {
font-size: 14px;
line-height: 1.5;
dt {
width: 72px;
}
dd {
flex: 1;
min-width: 0;
word-break: break-word;
}
& + dl {
margin-top: 12px;
}
}
}
}
.footer {
height: auto;
margin-top: 16px;
background-size: cover;
background-position: center;
padding: 28px 0 18px;
.button {
max-width: 100%;
width: 100%;
margin: 0 auto;
}
}
}
::v-deep(.drawLots-dialog) {
.bar {
left: 0;
width: 100%;
height: 20px;
}
h2 {
margin: 24px 0;
font-size: 22px;
}
.el-form {
margin-left: 0;
.el-form-item {
margin-bottom: 12px;
}
p {
font-size: 18px;
}
}
.el-button {
width: 100%;
max-width: 180px;
height: 48px;
font-size: 16px;
margin: 24px 0 12px;
}
}
}
</style>
......@@ -51,33 +51,38 @@ function handleChange(id: string, type: number) {
margin: 20px auto;
background: url(@/assets/images/home_expert_bg.png) no-repeat center center;
background-size: contain;
width: 1308px;
width: 100%;
max-width: 1308px;
height: 642px;
.link1 {
position: absolute;
bottom: 100px;
width: 400px;
width: 100%;
max-width: 400px;
height: 300px;
}
.link2 {
position: absolute;
left: 450px;
bottom: 50px;
width: 310px;
width: 100%;
max-width: 310px;
height: 250px;
}
.link3 {
position: absolute;
top: 0;
right: 80px;
width: 400px;
width: 100%;
max-width: 400px;
height: 300px;
}
.link4 {
position: absolute;
bottom: 0;
right: 0;
width: 400px;
width: 100%;
max-width: 400px;
height: 340px;
}
}
......@@ -94,4 +99,18 @@ function handleChange(id: string, type: number) {
margin-left: 20px;
}
}
@media (max-width: 1024px) {
.bg {
padding: 30px 16px;
}
}
@media (max-width: 767px) {
.bg {
padding: 20px 12px;
}
.link1,.link2,.link3,.link4 {
max-width: 100%;
margin-bottom: 16px;
}
}
</style>
......@@ -109,7 +109,8 @@ const learnURL = import.meta.env.VITE_SAAS_LEARN_URL
position: absolute;
top: 200px;
right: 80px;
width: 300px;
width: 100%;
max-width: 300px;
height: 300px;
}
}
......@@ -122,26 +123,30 @@ const learnURL = import.meta.env.VITE_SAAS_LEARN_URL
margin: 20px auto;
background: url(@/assets/images/home_student_bg.png) no-repeat center center;
background-size: contain;
width: 1215px;
width: 100%;
max-width: 1215px;
height: 577px;
.link1 {
position: absolute;
top: 160px;
width: 250px;
width: 100%;
max-width: 250px;
height: 250px;
}
.link2 {
position: absolute;
left: 400px;
bottom: 0;
width: 300px;
width: 100%;
max-width: 300px;
height: 250px;
}
.link3 {
position: absolute;
top: 200px;
right: 0;
width: 300px;
width: 100%;
max-width: 300px;
height: 300px;
}
}
......@@ -158,4 +163,13 @@ const learnURL = import.meta.env.VITE_SAAS_LEARN_URL
margin-left: 20px;
}
}
@media (max-width: 767px) {
.bg {
padding: 20px 12px;
}
.link1,.link2,.link3,.link4 {
max-width: 100%;
margin-bottom: 16px;
}
}
</style>
......@@ -72,33 +72,38 @@ function handleChange(id: string, type: number) {
margin: 20px auto;
background: url(@/assets/images/home_teacher_bg.png) no-repeat center center;
background-size: contain;
width: 1308px;
width: 100%;
max-width: 1308px;
height: 642px;
.link1 {
position: absolute;
bottom: 100px;
width: 400px;
width: 100%;
max-width: 400px;
height: 300px;
}
.link2 {
position: absolute;
left: 450px;
bottom: 50px;
width: 310px;
width: 100%;
max-width: 310px;
height: 250px;
}
.link3 {
position: absolute;
top: 0;
right: 80px;
width: 400px;
width: 100%;
max-width: 400px;
height: 300px;
}
.link4 {
position: absolute;
bottom: 0;
right: 0;
width: 400px;
width: 100%;
max-width: 400px;
height: 340px;
}
}
......@@ -115,4 +120,18 @@ function handleChange(id: string, type: number) {
margin-left: 20px;
}
}
@media (max-width: 1024px) {
.bg {
padding: 30px 16px;
}
}
@media (max-width: 767px) {
.bg {
padding: 20px 12px;
}
.link1,.link2,.link3,.link4 {
max-width: 100%;
margin-bottom: 16px;
}
}
</style>
......@@ -63,6 +63,8 @@ onMounted(() => {
.total {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
row-gap: 16px;
dl {
text-align: left;
}
......@@ -92,4 +94,24 @@ onMounted(() => {
color: #999;
}
}
@media (max-width: 767px) {
.total {
justify-content: flex-start;
gap: 8px;
padding: 0 8px;
}
.total .line {
display: none;
}
.total dl {
flex: 0 0 calc(50% - 4px);
box-sizing: border-box;
padding: 12px 8px;
background: #fff;
border-radius: 8px;
text-align: center;
}
.total dt span { font-size: 22px; }
.total dd { font-size: 13px; }
}
</style>
......@@ -66,12 +66,14 @@ const menus = computed(() => {
.card-list {
height: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 30px;
}
.card {
width: 310px;
width: 100%;
max-width: 310px;
height: 520px;
display: flex;
flex-direction: column;
......@@ -79,7 +81,8 @@ const menus = computed(() => {
justify-content: center;
background-color: #fff;
.icon {
width: 246px;
max-width: 246px;
width: 100%;
height: 238px;
img {
width: 100%;
......@@ -93,7 +96,8 @@ const menus = computed(() => {
color: #578ef9;
}
.button {
width: 130px;
max-width: 130px;
width: 100%;
height: 40px;
font-size: 16px;
line-height: 40px;
......@@ -106,4 +110,13 @@ const menus = computed(() => {
}
}
}
@media (max-width: 767px) {
.card-list {
flex-direction: column;
gap: 16px;
}
.card {
max-width: 100%;
}
}
</style>
......@@ -114,7 +114,7 @@ const goPage = function () {
<AppCard>
<div style="position: relative; z-index: 9">
<h1>2023年全国大学生商业数据分析与应用大赛</h1>
<el-form ref="formRef" :model="form" :rules="rules" label-width="124px" style="width: 600px; margin: 0 auto">
<el-form ref="formRef" :model="form" :rules="rules" label-width="124px" style="max-width: 600px; width: 100%; margin: 0 auto; padding: 0 16px;">
<!-- <el-form-item label="报名赛项">
<el-input v-model="form.name" disabled />
</el-form-item> -->
......@@ -209,14 +209,16 @@ const goPage = function () {
top: 10%;
left: 50%;
transform: translateX(-50%);
width: 202px;
max-width: 202px;
width: 100%;
}
.pop {
position: absolute;
top: 25%;
left: 50%;
transform: translateX(-50%);
width: 366px;
max-width: 366px;
width: 100%;
img {
width: 100%;
}
......@@ -226,7 +228,8 @@ const goPage = function () {
left: 50%;
transform: translateX(-50%);
.btn1 {
width: 170px;
max-width: 170px;
width: 100%;
line-height: 50px;
background: #e92d55;
border-radius: 40px;
......@@ -236,7 +239,8 @@ const goPage = function () {
cursor: pointer;
}
.btn2 {
width: 170px;
max-width: 170px;
width: 100%;
line-height: 50px;
border: 1px solid #e92d55;
border-radius: 40px;
......@@ -281,7 +285,8 @@ const goPage = function () {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
width: 100%;
max-width: 400px;
height: 450px;
background: #fff;
border-radius: 40px;
......@@ -290,10 +295,12 @@ const goPage = function () {
top: -25px;
left: 50%;
transform: translateX(-50%);
width: 276px;
max-width: 276px;
width: 100%;
}
.text-box {
width: 300px;
max-width: 300px;
width: 100%;
padding-top: 187px;
margin: 0 auto;
text-align: center;
......@@ -312,7 +319,8 @@ const goPage = function () {
.btn {
text-align: center;
margin: 0 auto;
width: 240px;
max-width: 240px;
width: 100%;
line-height: 88px;
background: #e92d55;
box-shadow: 0px 20px 30px #ffd3dc;
......@@ -325,4 +333,9 @@ const goPage = function () {
}
}
}
@media (max-width: 767px) {
.content {
padding: 20px 12px;
}
}
</style>
......@@ -68,6 +68,8 @@ const commentVisible = $ref(false)
.discuss-box-user {
display: flex;
align-items: center;
flex-wrap: wrap;
min-width: 0;
}
.discuss-box-user__avatar {
width: 40px;
......@@ -112,6 +114,7 @@ const commentVisible = $ref(false)
font-weight: 400;
line-height: 24px;
color: #333333;
word-break: break-word;
}
.discuss-box-footer {
display: flex;
......@@ -154,6 +157,7 @@ const commentVisible = $ref(false)
margin-left: 10px;
color: #333;
overflow: hidden;
word-break: break-word;
}
.discuss-comment__username {
font-size: 16px;
......@@ -167,4 +171,7 @@ const commentVisible = $ref(false)
color: #999999;
text-align: right;
}
@media (max-width: 767px) {
.discuss-item { padding: 12px 8px; }
}
</style>
......@@ -44,4 +44,6 @@ onMounted(() => {
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
@media (max-width: 1024px) { .contest-list { grid-template-columns: repeat(2, 1fr); gap: 16px; } }
@media (max-width: 767px) { .contest-list { grid-template-columns: 1fr; gap: 12px; } }
</style>
......@@ -113,7 +113,7 @@ function handleCancel() {
<template>
<AppCard title="大赛报名">
<el-form ref="formRef" :model="form" :rules="rules" label-width="124px" style="width: 600px; margin: 0 auto">
<el-form ref="formRef" :model="form" :rules="rules" label-width="124px" style="max-width: 600px; width: 100%; margin: 0 auto; padding: 0 12px;">
<el-form-item label="报名赛项">
<el-input v-model="form.name" disabled />
</el-form-item>
......
<script setup lang="ts">
import { getScoreReport, getMyContestList } from '../api'
import { useMediaQuery } from '@vueuse/core'
const isMobile = useMediaQuery('(max-width: 767px)')
let eventValue = $ref('')
let eventOption: any = $ref([])
......@@ -28,15 +31,23 @@ const selectChange = function () {
<template>
<AppCard>
<h1>2024 年湖北工匠杯成绩查询</h1>
<div style="display: flex; align-items: center; padding-top: 30px">
<div style="display: flex; align-items: center; padding-top: 30px; flex-wrap: wrap">
<span style="margin-right: 10px">赛项</span>
<el-select style="width: 320px" v-model="eventValue" placeholder="请选择" @change="selectChange">
<el-select style="width: 100%; max-width: 320px" v-model="eventValue" placeholder="请选择" @change="selectChange">
<el-option v-for="item in eventOption" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
<h3>考生基础信息</h3>
<!-- {{ scoreInfo?.id ? [scoreInfo] : [] }} -->
<el-table :data="scoreInfo?.id ? [scoreInfo] : []" stripe :header-cell-style="{ background: '#ededed' }">
<div v-if="isMobile && scoreInfo?.id" class="mobile-score-card">
<div class="mobile-score-card__row"><span>选手姓名</span><strong>{{ scoreInfo.student?.name || '-' }}</strong></div>
<div class="mobile-score-card__row"><span>参赛ID</span><strong>{{ scoreInfo.id || '-' }}</strong></div>
<div class="mobile-score-card__row"><span>性别</span><strong>{{ scoreInfo.student?.gender === 1 ? '男' : '女' }}</strong></div>
<div class="mobile-score-card__row"><span>学校</span><strong>{{ scoreInfo.org?.name || '-' }}</strong></div>
<div class="mobile-score-card__row"><span>所在专业</span><strong>{{ scoreInfo.specialty?.name || '-' }}</strong></div>
<div class="mobile-score-card__row"><span>所在年级</span><strong>{{ scoreInfo.grade || '-' }}</strong></div>
<div class="mobile-score-card__row"><span>联系电话</span><strong>{{ scoreInfo.student?.mobile || '-' }}</strong></div>
</div>
<el-table v-else :data="scoreInfo?.id ? [scoreInfo] : []" stripe :header-cell-style="{ background: '#ededed' }">
<el-table-column prop="student.name" label="选手姓名" />
<el-table-column prop="id" label="参赛ID" />
<el-table-column label="性别">
......@@ -57,7 +68,21 @@ const selectChange = function () {
{ label: '占比(%)', prop: 'ratio' },
{ label: '实际得分', prop: 'score' }
] -->
<el-table :data="scoreInfo?.score_detail" stripe :header-cell-style="{ background: '#ededed' }">
<div v-if="isMobile" class="mobile-score-list">
<div class="mobile-score-card" v-for="(item, index) in scoreInfo?.score_detail || []" :key="index">
<div class="mobile-score-card__row"><span>桌号</span><strong>{{ item.extension?.table_number || '-' }}</strong></div>
<div class="mobile-score-card__row"><span>考试账号</span><strong>{{ item.extension?.account || '-' }}</strong></div>
<div class="mobile-score-card__row">
<span>考试类型</span>
<strong>{{ item.extension?.type === '2' ? '商业数据分析师实操考试' : '理论考试' }}</strong>
</div>
<div class="mobile-score-card__row"><span>分值</span><strong>{{ item.old_score || '-' }}</strong></div>
<div class="mobile-score-card__row"><span>占比(%)</span><strong>{{ item.ratio || '-' }}</strong></div>
<div class="mobile-score-card__row"><span>实际得分</span><strong>{{ item.score || '-' }}</strong></div>
</div>
<el-empty v-if="!scoreInfo?.score_detail?.length" description="暂无成绩数据" />
</div>
<el-table v-else :data="scoreInfo?.score_detail" stripe :header-cell-style="{ background: '#ededed' }">
<el-table-column prop="extension.table_number" label="桌号" />
<el-table-column prop="extension.account" label="考试账号" />
<el-table-column prop="extension.type" label="考试类型">
......@@ -90,4 +115,37 @@ h3 {
padding-top: 40px;
line-height: 30px;
}
.mobile-score-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.mobile-score-card {
margin-top: 12px;
padding: 14px 16px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 10px;
}
.mobile-score-card__row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 0;
border-bottom: 1px solid #f3f4f6;
font-size: 14px;
}
.mobile-score-card__row:last-child {
border-bottom: none;
}
.mobile-score-card__row span {
flex-shrink: 0;
color: #909399;
}
.mobile-score-card__row strong {
min-width: 0;
text-align: right;
color: #303133;
word-break: break-word;
}
</style>
<script setup>
import { getExperimentRecord } from '../api'
import { useNow, useDateFormat } from '@vueuse/core'
import { useMediaQuery } from '@vueuse/core'
const props = defineProps({ experiment: Object, data: Object })
const isMobile = useMediaQuery('(max-width: 767px)')
const emit = defineEmits(['update', 'update:modelValue'])
......@@ -50,7 +52,24 @@ function handleRemove(row) {
<template>
<el-dialog append-to-body title="关联截图" width="800px" @update:modelValue="value => $emit('update:modelValue', value)">
<el-table :data="currentPictures" stripe>
<div v-if="isMobile" class="capture-card-list">
<div class="capture-card" v-for="row in currentPictures" :key="row.url">
<div class="capture-card__row">
<span>截图名称</span>
<el-link type="primary" :href="row.url" target="_blank">{{ row.name }}</el-link>
</div>
<div class="capture-card__row">
<span>上传时间</span>
<strong>{{ row.upload_time || '-' }}</strong>
</div>
<div class="capture-card__actions">
<el-button text type="danger" @click="handleRemove(row)" v-if="row.is_bind">取消关联</el-button>
<el-button text type="primary" @click="handleBind(row)" v-else>关联截图</el-button>
</div>
</div>
<el-empty v-if="!currentPictures.length" description="暂无截图" />
</div>
<el-table v-else :data="currentPictures" stripe>
<el-table-column prop="name">
<template #default="{ row }">
<el-link type="primary" :href="row.url" target="_blank"> {{ row.name }}</el-link>
......@@ -71,3 +90,41 @@ function handleRemove(row) {
</template>
</el-dialog>
</template>
<style scoped>
.capture-card-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.capture-card {
padding: 14px 16px;
border: 1px solid #ebeef5;
border-radius: 10px;
background: #fff;
}
.capture-card__row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 0;
border-bottom: 1px solid #f3f4f6;
}
.capture-card__row:last-of-type {
border-bottom: none;
}
.capture-card__row span {
flex-shrink: 0;
color: #909399;
}
.capture-card__row strong {
min-width: 0;
text-align: right;
word-break: break-word;
}
.capture-card__actions {
display: flex;
justify-content: flex-end;
padding-top: 8px;
}
</style>
......@@ -68,6 +68,8 @@ const commentVisible = $ref(false)
.discuss-box-user {
display: flex;
align-items: center;
flex-wrap: wrap;
min-width: 0;
}
.discuss-box-user__avatar {
width: 40px;
......@@ -112,6 +114,7 @@ const commentVisible = $ref(false)
font-weight: 400;
line-height: 24px;
color: #333333;
word-break: break-word;
}
.discuss-box-footer {
display: flex;
......@@ -154,6 +157,7 @@ const commentVisible = $ref(false)
margin-left: 10px;
color: #333;
overflow: hidden;
word-break: break-word;
}
.discuss-comment__username {
font-size: 16px;
......@@ -167,4 +171,7 @@ const commentVisible = $ref(false)
color: #999999;
text-align: right;
}
@media (max-width: 767px) {
.discuss-item { padding: 12px 8px; }
}
</style>
......@@ -3,7 +3,7 @@ const props = defineProps({ data: Object })
const teacherText = $computed(() => {
if (!props.data) return ''
return props.data.teachers.map(item => item.name).join('、')
return props.data.teachers.map((item) => item.name).join('、')
})
</script>
......@@ -20,10 +20,10 @@ const teacherText = $computed(() => {
</el-form>
<el-divider />
<el-form label-suffix=":" label-position="top">
<el-form-item label="实验目的">{{ data.purpose }}</el-form-item>
<el-form-item label="实验要求">{{ data.requirements }}</el-form-item>
<el-form-item label="实验内容及原理">{{ data.content }}</el-form-item>
<el-form-item label="实验步骤及过程">{{ data.procedure }}</el-form-item>
<el-form-item label="实验目的" v-if="data.purpose">{{ data.purpose }}</el-form-item>
<el-form-item label="实验要求" v-if="data.requirements">{{ data.requirements }}</el-form-item>
<el-form-item label="实验内容及原理" v-if="data.content">{{ data.content }}</el-form-item>
<el-form-item label="实验步骤及过程" v-if="data.procedure">{{ data.procedure }}</el-form-item>
</el-form>
</template>
<el-empty description="暂无数据" v-else />
......
......@@ -18,26 +18,26 @@ const detail = $ref(inject<ExperimentRecord>('detail'))
<el-dialog title="查看实验报告" :close-on-click-modal="false" width="600px">
<el-form label-suffix=":" label-width="110px" label-position="right">
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验名称">{{ data.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验课程名称">{{ data.course.name }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="学生姓名">{{ data.student.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="学生学号">{{ data.student.sno_number }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="所属专业">{{ data.student.specialty.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="所属班级">{{ classText }}</el-form-item>
</el-col>
</el-row>
......
<script setup>
import { getExperimentReport } from '../api'
import { useMediaQuery } from '@vueuse/core'
const props = defineProps({ id: String, makePdf: Boolean })
const emit = defineEmits(['ready'])
const isMobile = useMediaQuery('(max-width: 767px)')
let experiment = $ref()
let report = $ref()
......@@ -62,42 +64,42 @@ function handleClose() {
<h1 class="report-title">实验报告</h1>
<el-form label-suffix=":" hide-required-asterisk v-if="experiment">
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="课程名称">{{ experiment.course.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验名称">{{ experiment.name }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验地点">{{ report.experiment_address }} </el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验日期"> {{ report.experiment_date }} </el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验类型">{{ experiment.type_level }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="指导教师">{{ teacherText }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="专业">{{ experiment.student.specialty.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="班级">{{ classText }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="姓名">{{ experiment.student.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="学号">{{ experiment.student.sno_number }}</el-form-item>
</el-col>
</el-row>
......@@ -121,7 +123,23 @@ function handleClose() {
</template>
<!-- 附件 -->
<template v-if="item.type === 2">
<div v-if="isMobile && !makePdf" class="report-file-card-list">
<div class="report-file-card" v-for="(file, fileIndex) in item.files" :key="file.id || file.url || fileIndex">
<div class="report-file-card__row"><span>序号</span><strong>{{ fileIndex + 1 }}</strong></div>
<div class="report-file-card__row">
<span>附件类型</span>
<strong>{{ file.type === '1' ? '本地上传' : '实验截图' }}</strong>
</div>
<div class="report-file-card__row"><span>附件名称</span><strong>{{ file.name || '-' }}</strong></div>
<div class="report-file-card__row"><span>更新时间</span><strong>{{ file.updated_time || '-' }}</strong></div>
<div class="report-file-card__actions">
<el-button text type="primary"><a :href="file.url" target="_blank">查看</a></el-button>
</div>
</div>
<el-empty v-if="!item.files?.length" description="暂无附件" />
</div>
<el-table
v-else
:data="item.files"
stripe
:header-cell-style="{ background: '#ededed' }"
......@@ -221,4 +239,60 @@ function handleClose() {
border: 1px solid var(--main-color);
border-radius: 6px;
}
.report-file-card-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 20px;
}
.report-file-card {
padding: 14px 16px;
border: 1px solid #ebeef5;
border-radius: 10px;
background: #fff;
}
.report-file-card__row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 0;
border-bottom: 1px solid #f3f4f6;
}
.report-file-card__row:last-of-type {
border-bottom: none;
}
.report-file-card__row span {
flex-shrink: 0;
color: #909399;
}
.report-file-card__row strong {
min-width: 0;
text-align: right;
word-break: break-word;
}
.report-file-card__actions {
display: flex;
justify-content: flex-end;
padding-top: 8px;
}
@media (max-width: 767px) {
.report-preview__inner {
padding: 20px 12px;
}
.report-title {
padding: 12px 0 20px;
}
.report-form-item {
margin-bottom: 24px;
.form-hd {
flex-direction: column;
gap: 6px;
align-items: flex-start;
}
}
.report-score {
margin: 24px 0;
font-size: 20px;
}
}
</style>
......@@ -203,4 +203,38 @@ h3 {
}
}
}
@media (max-width: 767px) {
.picture-list {
li {
height: auto;
margin: 16px 0;
img {
display: block;
aspect-ratio: 16 / 9;
}
p {
position: static;
padding: 8px 10px 0;
color: #666;
text-align: left;
background: none;
}
}
.cover {
display: block;
position: static;
background: none;
}
.cover-inner {
flex-direction: row;
justify-content: flex-start;
gap: 10px;
padding: 10px;
.el-button {
width: auto;
margin: 0;
}
}
}
}
</style>
<script setup lang="ts">
import { getExperimentScore } from '../api'
import { useMediaQuery } from '@vueuse/core'
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
const isMobile = useMediaQuery('(max-width: 767px)')
const ResultScoreViewAutoDialog = defineAsyncComponent(() => import('./ResultScoreViewAutoDialog.vue'))
......@@ -95,30 +97,30 @@ function handleViewAuto(row: any) {
<el-form-item label="实验名称">{{ experiment.name }}</el-form-item>
<el-form-item label="实验课程名称">{{ experiment.course.name }}</el-form-item>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="学生姓名">{{ experiment.student.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="学生学号">{{ experiment.student.sno_number }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="所属专业">{{ experiment.student.specialty.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="所属班级">{{ classText }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="评分教师">
{{
detail.checker_sso_user.real_name || detail.checker_sso_user.nickname || detail.checker_sso_user.username
}}
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="评分时间">{{ detail.check_time }}</el-form-item>
</el-col>
</el-row>
......@@ -127,11 +129,34 @@ function handleViewAuto(row: any) {
<p>实验得分</p>
<p class="t1">{{ detail.score }}</p>
</div>
<div v-if="detail.is_show === '1' && isMobile" class="score-detail-card-list">
<div class="score-detail-card" v-for="row in detail.score_details" :key="`${row.type}-${row.name}`">
<div class="score-detail-card__row"><span>实验成绩组成项</span><strong>{{ row.name || '-' }}</strong></div>
<div class="score-detail-card__row"><span>权重</span><strong>{{ row.percent }}%</strong></div>
<div class="score-detail-card__row">
<span>得分</span>
<strong>
{{ row.commit_score }}
<template v-if="(row.type === 1 && experiment.report_upload_way === 2) || row.type === 8">
<el-button text type="primary">
<a :href="getOperationUrl(row.type)" target="_blank">查看明细</a>
</el-button>
</template>
</strong>
</div>
<div class="score-detail-card__actions">
<el-button type="primary" link @click="handleViewAuto(row)" v-if="[6, 7, 9, 11, 12].includes(row.type)">查看标准答案</el-button>
<el-button link type="primary">
<a :href="getOperationUrl(row.type)" target="_blank">查看我的答案</a>
</el-button>
</div>
</div>
</div>
<el-table
:data="detail.score_details"
stripe
:header-cell-style="{ background: '#ededed' }"
v-if="detail.is_show === '1'">
v-else-if="detail.is_show === '1'">
<el-table-column label="实验成绩组成项" prop="name" align="center"></el-table-column>
<el-table-column label="权重" prop="percent" align="center">
<template #default="{ row }">{{ row.percent }}%</template>
......@@ -188,6 +213,7 @@ function handleViewAuto(row: any) {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
}
.form-result__score {
padding: 40px;
......@@ -198,4 +224,51 @@ function handleViewAuto(row: any) {
color: var(--main-color);
}
}
.score-detail-card-list {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.score-detail-card {
padding: 14px 16px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 10px;
}
.score-detail-card__row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 0;
border-bottom: 1px solid #f3f4f6;
}
.score-detail-card__row:last-of-type {
border-bottom: none;
}
.score-detail-card__row span {
flex-shrink: 0;
color: #909399;
}
.score-detail-card__row strong {
min-width: 0;
text-align: right;
word-break: break-word;
}
.score-detail-card__actions {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: 8px;
padding-top: 8px;
}
@media (max-width: 767px) {
.form-result {
flex-direction: column;
align-items: stretch;
}
.form-result__score {
padding: 20px;
}
}
</style>
......@@ -185,11 +185,16 @@ useEventListener(window, 'message', async (event) => {
width: 100%;
margin-top: 20px;
display: flex;
align-items: stretch;
min-height: 0;
}
.iframe {
display: block;
flex: 1;
width: 100%;
height: 100%;
min-height: 100%;
}
.welcome-box {
......@@ -198,7 +203,8 @@ useEventListener(window, 'message', async (event) => {
.entry-btn {
display: block;
width: 300px;
max-width: 300px;
width: 100%;
margin: 100px auto;
}
}
......@@ -214,6 +220,7 @@ useEventListener(window, 'message', async (event) => {
.welcome-box-header-content {
width: 100%;
max-width: 1000px;
padding: 0 16px;
margin: 0 auto;
color: #fff;
padding: 40px 0;
......@@ -253,4 +260,22 @@ useEventListener(window, 'message', async (event) => {
text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.5);
}
}
@media (max-width: 767px) {
.iframe-box {
margin-top: 12px;
display: block;
height: 74vh;
min-height: 74vh;
overflow: hidden;
}
.iframe {
height: 74vh;
min-height: 74vh;
}
.welcome-box-header {
padding: 40px 16px;
}
}
</style>
......@@ -2,6 +2,7 @@
import type { CourseType, ExperimentInfo, ExperimentRecord } from '../types'
import { HomeFilled } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { useMediaQuery } from '@vueuse/core'
import DragPanel from '@/components/DragPanel.vue'
import { useGetCourseList } from '../composables/useGetCourseList'
import { upload } from '@/utils/upload'
......@@ -32,6 +33,7 @@ const Exam = defineAsyncComponent(() => import('../components/Exam.vue'))
const Example = defineAsyncComponent(() => import('../components/Example.vue'))
const route = useRoute()
const isMobile = useMediaQuery('(max-width: 767px)')
// 左侧
const form = reactive<{ course_id: string; experiment_id: string }>({
......@@ -103,6 +105,19 @@ watchEffect(() => {
})
const tabActive = ref('info')
const mobilePanelExpanded = ref(false)
const tabLabelMap: Record<string, string> = {
info: '实验信息',
example: '实验案例',
case: '案例原文',
book: '实训指导',
video: '操作视频',
exam: '理论试题',
qa: '实操试题',
result: '过程与结果',
discuss: '讨论交流',
}
const currentTabLabel = computed(() => tabLabelMap[tabActive.value] || '实验面板')
const examURL = ref('')
// 右侧
......@@ -258,6 +273,7 @@ function handleGoToFunction(path: string) {
<DragPanel @resize="handleResize" :isLeftShow="experimentInfo?.exam_status === 1">
<template #left>
<div class="lab-left">
<div class="lab-left-body">
<el-form :model="form" label-suffix=":" hide-required-asterisk>
<el-form-item label="请选择课程">
<el-select :model-value="form.course_id" @change="handleCourseChange" style="width: 100%">
......@@ -270,7 +286,13 @@ function handleGoToFunction(path: string) {
</el-select>
</el-form-item>
</el-form>
<el-tabs type="border-card" v-model="tabActive" v-if="experimentInfo">
<div class="mobile-panel-toggle" v-if="isMobile && experimentInfo">
<span>当前面板:{{ currentTabLabel }}</span>
<el-button text type="primary" @click="mobilePanelExpanded = !mobilePanelExpanded">
{{ mobilePanelExpanded ? '收起' : '展开' }}
</el-button>
</div>
<el-tabs type="border-card" v-model="tabActive" v-if="experimentInfo" v-show="!isMobile || mobilePanelExpanded">
<el-tab-pane label="实验信息" name="info">
<Info :data="experimentInfo"></Info>
</el-tab-pane>
......@@ -319,6 +341,7 @@ function handleGoToFunction(path: string) {
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<template #right>
<AppCard>
......@@ -326,14 +349,14 @@ function handleGoToFunction(path: string) {
<el-button type="primary" @click="tabActive = 'exam'">理论试题</el-button>
<el-button type="primary" @click="tabActive = 'qa'">实操环境</el-button>
</div>
<el-row justify="space-between" v-else>
<div>
<el-row justify="space-between" v-else class="lab-toolbar">
<div class="lab-toolbar-group">
<el-button type="primary" :icon="HomeFilled" :disabled="submitted" @click="handleBackHome"
>返回首页</el-button
>
<el-button type="primary" :disabled="disabled" @click="handleSubmit">提交实验</el-button>
</div>
<div>
<div class="lab-toolbar-group">
<el-button type="primary" v-if="appConfig.system == 'default'">
<router-link to="/ai" target="_blank">AI数据分析</router-link>
</el-button>
......@@ -392,7 +415,7 @@ function handleGoToFunction(path: string) {
class="iframe"
ref="iframeRef"></iframe>
<div
style="padding: 10px; background-color: #fff; width: 300px"
style="padding: 10px; background-color: #fff; width: 100%; max-width: 300px"
v-if="experimentInfo?.exam_status === 1 && tabActive === 'qa'">
<Question :experiment_id="form.experiment_id" :exam_status="experimentInfo?.exam_status"></Question>
</div>
......@@ -459,15 +482,119 @@ function handleGoToFunction(path: string) {
overflow-y: auto;
}
}
.lab-left-body {
display: flex;
flex: 1;
min-height: 0;
flex-direction: column;
}
.mobile-panel-toggle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
padding: 10px 12px;
background: #fff;
border-radius: 8px;
span {
flex: 1;
min-width: 0;
color: #606266;
font-size: 14px;
line-height: 20px;
}
}
.lab-toolbar {
gap: 12px;
}
.lab-toolbar-group {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.exam-status {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.lab-box {
position: relative;
flex: 1;
width: 100%;
margin-top: 20px;
display: flex;
align-items: stretch;
min-height: 0;
}
.iframe {
display: block;
flex: 1;
width: 100%;
height: 100%;
min-height: 100%;
}
@media (max-width: 767px) {
.lab-left {
:deep(.el-form-item) {
margin-bottom: 12px;
}
:deep(.el-tabs__header) {
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
:deep(.el-tabs__header::-webkit-scrollbar) {
display: none;
}
:deep(.el-tabs__nav-wrap) {
overflow: visible;
}
:deep(.el-tabs__nav) {
flex-wrap: nowrap;
}
:deep(.el-tabs__item) {
flex-shrink: 0;
min-width: max-content;
padding: 0 12px !important;
font-size: 14px;
}
:deep(.el-tabs__content) {
height: auto;
max-height: 52vh;
padding: 12px;
overflow-y: auto;
}
}
.lab-toolbar {
flex-direction: column;
align-items: stretch;
}
.lab-toolbar-group,
.exam-status {
width: 100%;
}
.lab-toolbar-group :deep(.el-button),
.exam-status :deep(.el-button) {
flex: 1 1 calc(50% - 6px);
min-width: calc(50% - 6px);
margin-left: 0;
}
.lab-toolbar-group :deep(.el-button > span),
.exam-status :deep(.el-button > span) {
justify-content: center;
}
.lab-box {
margin-top: 12px;
display: block;
height: 74vh;
min-height: 74vh;
overflow: hidden;
}
.iframe {
height: 74vh;
min-height: 74vh;
}
}
</style>
......@@ -3,6 +3,7 @@ import AppEditor from '@/components/base/AppEditor.vue'
import { useMapStore } from '@/stores/map'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useFileDialog } from '@vueuse/core'
import { useMediaQuery } from '@vueuse/core'
import { upload } from '@/utils/upload'
import { useNow, useDateFormat } from '@vueuse/core'
import {
......@@ -15,6 +16,7 @@ import {
import BindCaptureDialog from '../components/BindCaptureDialog.vue'
const props = defineProps({ id: String })
const isMobile = useMediaQuery('(max-width: 767px)')
const experimentAddress = useMapStore().getMapValuesByKey('experiment_address')
......@@ -163,48 +165,48 @@ function handleUpdateBindCapture(files) {
hide-required-asterisk
v-if="experiment">
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="课程名称">{{ experiment.course.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验名称">{{ experiment.name }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验地点">
<el-select v-model="form.experiment_address">
<el-option v-for="item in experimentAddress" v-bind="item" :key="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验日期">
<el-date-picker v-model="form.experiment_date" value-format="YYYY-MM-DD"></el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="实验类型">{{ experiment.type_level }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="指导教师">{{ teacherText }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="专业">{{ experiment.student.specialty.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="班级">{{ classText }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="姓名">{{ experiment.student.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-col :xs="24" :sm="24" :md="12">
<el-form-item label="学号">{{ experiment.student.sno_number }}</el-form-item>
</el-col>
</el-row>
......@@ -221,7 +223,23 @@ function handleUpdateBindCapture(files) {
<el-button type="primary" @click="handleBindCapture(item)" v-if="item.categories.includes(1)"
>关联截图</el-button
>
<el-table :data="item.files" stripe :header-cell-style="{ background: '#ededed' }" style="margin-top: 20px">
<div v-if="isMobile" class="report-file-card-list">
<div class="report-file-card" v-for="(file, fileIndex) in item.files || []" :key="file.id || file.url || fileIndex">
<div class="report-file-card__row"><span>序号</span><strong>{{ fileIndex + 1 }}</strong></div>
<div class="report-file-card__row">
<span>附件类型</span>
<strong>{{ file.type === '1' ? '本地上传' : '实验截图' }}</strong>
</div>
<div class="report-file-card__row"><span>附件名称</span><strong>{{ file.name || '-' }}</strong></div>
<div class="report-file-card__row"><span>更新时间</span><strong>{{ file.updated_time || '-' }}</strong></div>
<div class="report-file-card__actions">
<el-button text type="primary"><a :href="file.url" target="_blank">查看</a></el-button>
<el-button text type="danger" @click="handleRemoveFile(item, fileIndex)">删除</el-button>
</div>
</div>
<el-empty v-if="!item.files?.length" description="暂无附件" />
</div>
<el-table v-else :data="item.files" stripe :header-cell-style="{ background: '#ededed' }" style="margin-top: 20px">
<el-table-column label="序号" type="index" width="80" align="center"></el-table-column>
<el-table-column label="附件类型" prop="type" align="center">
<template #default="{ row }">
......@@ -274,4 +292,47 @@ function handleUpdateBindCapture(files) {
justify-content: flex-start;
}
}
.report-file-card-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 20px;
}
.report-file-card {
padding: 14px 16px;
border: 1px solid #ebeef5;
border-radius: 10px;
background: #fff;
}
.report-file-card__row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 0;
border-bottom: 1px solid #f3f4f6;
}
.report-file-card__row:last-of-type {
border-bottom: none;
}
.report-file-card__row span {
flex-shrink: 0;
color: #909399;
}
.report-file-card__row strong {
min-width: 0;
text-align: right;
word-break: break-word;
}
.report-file-card__actions {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: 8px;
padding-top: 8px;
}
@media (max-width: 767px) {
.report-title {
padding: 12px 0 20px;
}
}
</style>
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论