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

feat: 页面响应式设计

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