|
|
<script setup lang="ts">
|
|
|
/**
|
|
|
* 与原型 frontend/prototype/pages/admin-reviewers.html(layout.html role=admin)一致:
|
|
|
* 左侧赛道配置 Tab + 右侧当前赛道评审员表(姓名/电话/账户/密码/修改|删除)。
|
|
|
* 数据来源:后端 reviewers + reviewer_scopes(每场赛事由顶栏切换器选定)。
|
|
|
*/
|
|
|
import { computed, ref, watch } from 'vue'
|
|
|
import { storeToRefs } from 'pinia'
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
import type { FormInstance, FormRules } from 'element-plus'
|
|
|
import { listTracks } from '../../../api/admin/competitions'
|
|
|
import { createReviewer, deleteReviewer, updateReviewer } from '../../../api/admin/reviewers'
|
|
|
import {
|
|
|
createReviewerScope,
|
|
|
deleteReviewerScope,
|
|
|
fetchReviewerScopeTrackCounts,
|
|
|
listReviewerScopes,
|
|
|
} from '../../../api/admin/reviewerScopes'
|
|
|
import type { CompetitionTrackRow, ReviewerScopeRow } from '../../../api/admin/types'
|
|
|
import { useAdminCompetitionStore } from '../../../stores/adminCompetition'
|
|
|
|
|
|
const competitionStore = useAdminCompetitionStore()
|
|
|
const { selectedCompetitionId } = storeToRefs(competitionStore)
|
|
|
|
|
|
const tracks = ref<CompetitionTrackRow[]>([])
|
|
|
const trackCounts = ref<Record<string, number>>({})
|
|
|
const currentTrackCode = ref<string | null>(null)
|
|
|
const scopeRows = ref<ReviewerScopeRow[]>([])
|
|
|
const shellLoading = ref(false)
|
|
|
const tableLoading = ref(false)
|
|
|
|
|
|
const editDialog = ref(false)
|
|
|
const editMode = ref<'create' | 'edit'>('create')
|
|
|
const saving = ref(false)
|
|
|
const editingReviewerId = ref<number | null>(null)
|
|
|
|
|
|
const formRef = ref<FormInstance>()
|
|
|
const form = ref({
|
|
|
name: '',
|
|
|
mobile: '',
|
|
|
username: '',
|
|
|
password: '',
|
|
|
})
|
|
|
|
|
|
const rules: FormRules = {
|
|
|
name: [{ required: true, message: '请填写姓名', trigger: 'blur' }],
|
|
|
username: [
|
|
|
{ required: true, message: '请填写账户', trigger: 'blur' },
|
|
|
{ pattern: /^[A-Za-z0-9._-]+$/, message: '仅字母、数字、点、横线或下划线', trigger: 'blur' },
|
|
|
],
|
|
|
password: [
|
|
|
{
|
|
|
validator: (_rule, val, cb) => {
|
|
|
if (editMode.value === 'create' && (!val || String(val).trim() === '')) {
|
|
|
cb(new Error('请填写密码'))
|
|
|
return
|
|
|
}
|
|
|
if (val && String(val).trim().length > 0 && String(val).length < 6) {
|
|
|
cb(new Error('密码不少于 6 位'))
|
|
|
return
|
|
|
}
|
|
|
cb()
|
|
|
},
|
|
|
trigger: 'blur',
|
|
|
},
|
|
|
],
|
|
|
}
|
|
|
|
|
|
const currentTrackTitle = computed(
|
|
|
() => tracks.value.find((t) => t.track_code === currentTrackCode.value)?.title ?? '',
|
|
|
)
|
|
|
|
|
|
const currentMeta = computed(() => {
|
|
|
const n = currentTrackCode.value ? (trackCounts.value[currentTrackCode.value] ?? 0) : 0
|
|
|
return `已配置 ${n} 名评审员`
|
|
|
})
|
|
|
|
|
|
async function loadTracks() {
|
|
|
const cid = selectedCompetitionId.value
|
|
|
shellLoading.value = true
|
|
|
if (!cid) {
|
|
|
tracks.value = []
|
|
|
trackCounts.value = {}
|
|
|
currentTrackCode.value = null
|
|
|
shellLoading.value = false
|
|
|
return
|
|
|
}
|
|
|
try {
|
|
|
tracks.value = (await listTracks(cid)).filter((t) => t.is_enabled).sort((a, b) => a.sort - b.sort)
|
|
|
if (!tracks.value.length) {
|
|
|
currentTrackCode.value = null
|
|
|
trackCounts.value = {}
|
|
|
return
|
|
|
}
|
|
|
if (!currentTrackCode.value || !tracks.value.some((t) => t.track_code === currentTrackCode.value)) {
|
|
|
currentTrackCode.value = tracks.value[0]?.track_code ?? null
|
|
|
}
|
|
|
trackCounts.value = await fetchReviewerScopeTrackCounts(cid)
|
|
|
} catch (e) {
|
|
|
tracks.value = []
|
|
|
ElMessage.warning(e instanceof Error ? e.message : '赛道加载失败')
|
|
|
} finally {
|
|
|
shellLoading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function loadTable() {
|
|
|
const cid = selectedCompetitionId.value
|
|
|
const tc = currentTrackCode.value
|
|
|
if (!cid || !tc) {
|
|
|
scopeRows.value = []
|
|
|
return
|
|
|
}
|
|
|
tableLoading.value = true
|
|
|
try {
|
|
|
const res = await listReviewerScopes({
|
|
|
competition_id: cid,
|
|
|
track_code: tc,
|
|
|
page: 1,
|
|
|
per_page: 500,
|
|
|
})
|
|
|
scopeRows.value = res.data
|
|
|
trackCounts.value = await fetchReviewerScopeTrackCounts(cid)
|
|
|
} catch (e) {
|
|
|
scopeRows.value = []
|
|
|
ElMessage.error(e instanceof Error ? e.message : '加载失败')
|
|
|
} finally {
|
|
|
tableLoading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function selectTab(trackCode: string) {
|
|
|
currentTrackCode.value = trackCode
|
|
|
}
|
|
|
|
|
|
watch(
|
|
|
selectedCompetitionId,
|
|
|
() => {
|
|
|
currentTrackCode.value = null
|
|
|
void loadTracks().then(() => loadTable())
|
|
|
},
|
|
|
{ immediate: true },
|
|
|
)
|
|
|
|
|
|
watch(currentTrackCode, () => {
|
|
|
void loadTable()
|
|
|
})
|
|
|
|
|
|
function pwdDisplay(row: ReviewerScopeRow): string {
|
|
|
return row.reviewer?.password_display ?? '—'
|
|
|
}
|
|
|
|
|
|
function telDisplay(row: ReviewerScopeRow): string {
|
|
|
const m = row.reviewer?.mobile
|
|
|
return m && String(m).trim() !== '' ? String(m) : '—'
|
|
|
}
|
|
|
|
|
|
function openCreate() {
|
|
|
editMode.value = 'create'
|
|
|
editingReviewerId.value = null
|
|
|
form.value = { name: '', mobile: '', username: '', password: '' }
|
|
|
editDialog.value = true
|
|
|
}
|
|
|
|
|
|
function openEdit(row: ReviewerScopeRow) {
|
|
|
editMode.value = 'edit'
|
|
|
editingReviewerId.value = row.reviewer_id
|
|
|
const r = row.reviewer
|
|
|
form.value = {
|
|
|
name: r?.name ?? '',
|
|
|
mobile: r?.mobile ?? '',
|
|
|
username: r?.username ?? '',
|
|
|
password: '',
|
|
|
}
|
|
|
editDialog.value = true
|
|
|
}
|
|
|
|
|
|
async function submitEdit() {
|
|
|
if (!formRef.value) return
|
|
|
const cid = selectedCompetitionId.value
|
|
|
const tc = currentTrackCode.value
|
|
|
if (!cid || !tc) return
|
|
|
|
|
|
await formRef.value.validate(async (ok) => {
|
|
|
if (!ok) return
|
|
|
saving.value = true
|
|
|
try {
|
|
|
if (editMode.value === 'create') {
|
|
|
let reviewerId = 0
|
|
|
try {
|
|
|
const created = await createReviewer({
|
|
|
name: form.value.name.trim(),
|
|
|
username: form.value.username.trim(),
|
|
|
password: form.value.password,
|
|
|
mobile: form.value.mobile.trim() || null,
|
|
|
})
|
|
|
reviewerId = created.id
|
|
|
} catch (e) {
|
|
|
ElMessage.error(e instanceof Error ? e.message : '创建评审员失败')
|
|
|
saving.value = false
|
|
|
return
|
|
|
}
|
|
|
try {
|
|
|
await createReviewerScope({
|
|
|
reviewer_id: reviewerId,
|
|
|
competition_id: cid,
|
|
|
track_code: tc,
|
|
|
})
|
|
|
} catch (e) {
|
|
|
try {
|
|
|
await deleteReviewer(reviewerId)
|
|
|
} catch {
|
|
|
/* 回滚失败时仍提示绑定错误 */
|
|
|
}
|
|
|
ElMessage.error(e instanceof Error ? e.message : '绑定赛道失败')
|
|
|
saving.value = false
|
|
|
return
|
|
|
}
|
|
|
ElMessage.success('评审员信息已保存')
|
|
|
} else {
|
|
|
const rid = editingReviewerId.value!
|
|
|
await updateReviewer(rid, {
|
|
|
name: form.value.name.trim(),
|
|
|
username: form.value.username.trim(),
|
|
|
mobile: form.value.mobile.trim() || null,
|
|
|
...(form.value.password.trim() ? { password: form.value.password.trim() } : {}),
|
|
|
})
|
|
|
ElMessage.success('评审员信息已更新')
|
|
|
}
|
|
|
editDialog.value = false
|
|
|
await loadTable()
|
|
|
if (cid) {
|
|
|
trackCounts.value = await fetchReviewerScopeTrackCounts(cid)
|
|
|
}
|
|
|
} catch (e) {
|
|
|
ElMessage.error(e instanceof Error ? e.message : '保存失败')
|
|
|
} finally {
|
|
|
saving.value = false
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
|
|
|
async function onDelete(row: ReviewerScopeRow) {
|
|
|
const name = row.reviewer?.name ?? '该评审员'
|
|
|
try {
|
|
|
await ElMessageBox.confirm(`确定移除「${name}」对本赛道的评审权限吗?(不删除全局账号)`, '确认删除', {
|
|
|
type: 'warning',
|
|
|
confirmButtonText: '确定删除',
|
|
|
cancelButtonText: '取消',
|
|
|
})
|
|
|
} catch {
|
|
|
return
|
|
|
}
|
|
|
try {
|
|
|
await deleteReviewerScope(row.id)
|
|
|
ElMessage.success('已移除')
|
|
|
await loadTable()
|
|
|
const cid = selectedCompetitionId.value
|
|
|
if (cid) {
|
|
|
trackCounts.value = await fetchReviewerScopeTrackCounts(cid)
|
|
|
}
|
|
|
} catch (e) {
|
|
|
ElMessage.error(e instanceof Error ? e.message : '删除失败')
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<div class="proto-reviewer-page admin-desktop-page">
|
|
|
<div class="rev-head">
|
|
|
<h5 class="section-title mb-0">评审员管理</h5>
|
|
|
</div>
|
|
|
|
|
|
<el-alert
|
|
|
v-if="!selectedCompetitionId"
|
|
|
type="warning"
|
|
|
show-icon
|
|
|
:closable="false"
|
|
|
class="rev-alert"
|
|
|
title="请先在顶栏「赛事切换」中选择一场赛事,再按赛道维护评审员。"
|
|
|
/>
|
|
|
|
|
|
<template v-else>
|
|
|
<div v-loading="shellLoading" class="reviewer-manage-shell">
|
|
|
<aside class="reviewer-track-panel">
|
|
|
<div class="reviewer-track-panel-title">赛道配置</div>
|
|
|
<div class="reviewer-track-tabs">
|
|
|
<button
|
|
|
v-for="t in tracks"
|
|
|
:key="t.id"
|
|
|
type="button"
|
|
|
class="reviewer-track-tab"
|
|
|
:class="{ active: currentTrackCode === t.track_code }"
|
|
|
@click="selectTab(t.track_code)"
|
|
|
>
|
|
|
<span class="reviewer-track-tab-name">{{ t.title }}</span>
|
|
|
<span class="reviewer-track-tab-count">{{ trackCounts[t.track_code] ?? 0 }} 人</span>
|
|
|
</button>
|
|
|
</div>
|
|
|
</aside>
|
|
|
|
|
|
<section class="reviewer-editor-panel">
|
|
|
<div class="reviewer-editor-head">
|
|
|
<div>
|
|
|
<h6 class="reviewer-track-title">{{ currentTrackTitle || '—' }}</h6>
|
|
|
<p class="reviewer-track-meta mb-0">{{ currentMeta }}</p>
|
|
|
</div>
|
|
|
<div class="reviewer-editor-actions">
|
|
|
<button type="button" class="prm-btn-outline" :disabled="!currentTrackCode" @click="openCreate">
|
|
|
新增评审员
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div v-loading="tableLoading" class="table-responsive">
|
|
|
<table v-if="currentTrackCode" class="table table-sm align-middle mb-0 reviewer-manage-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th style="width: 18%">姓名</th>
|
|
|
<th style="width: 22%">电话</th>
|
|
|
<th style="width: 24%">账户</th>
|
|
|
<th style="width: 24%">密码</th>
|
|
|
<th class="text-end" style="width: 12%">操作</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-for="row in scopeRows" :key="row.id">
|
|
|
<td>{{ row.reviewer?.name ?? '—' }}</td>
|
|
|
<td>{{ telDisplay(row) }}</td>
|
|
|
<td>{{ row.reviewer?.username ?? '—' }}</td>
|
|
|
<td>{{ pwdDisplay(row) }}</td>
|
|
|
<td class="text-end reviewer-row-actions">
|
|
|
<button type="button" class="prm-btn-outline sm" @click="openEdit(row)">修改</button>
|
|
|
<button type="button" class="prm-btn-light sm" @click="onDelete(row)">删除</button>
|
|
|
</td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
<div v-if="!currentTrackCode" class="empty-hint">
|
|
|
{{
|
|
|
tracks.length === 0
|
|
|
? '本场暂无启用中的赛道,请先在「赛事中心」配置赛道后再维护评审员。'
|
|
|
: ''
|
|
|
}}
|
|
|
</div>
|
|
|
</div>
|
|
|
<p class="footnote">
|
|
|
正式环境仅存密码摘要;列表中为占位符而非明文密码。
|
|
|
</p>
|
|
|
</section>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<el-dialog
|
|
|
v-model="editDialog"
|
|
|
:title="editMode === 'create' ? '新增评审员' : '修改评审员'"
|
|
|
width="520px"
|
|
|
destroy-on-close
|
|
|
class="rev-dialog"
|
|
|
>
|
|
|
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="rev-form">
|
|
|
<el-row :gutter="12">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="姓名" prop="name" required>
|
|
|
<el-input v-model="form.name" maxlength="64" placeholder="必填" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="电话" prop="mobile">
|
|
|
<el-input v-model="form.mobile" maxlength="20" placeholder="可选" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="账户" prop="username" required>
|
|
|
<el-input v-model="form.username" maxlength="64" autocomplete="off" placeholder="必填" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item :label="editMode === 'create' ? '密码' : '密码(不修改请留空)'" prop="password">
|
|
|
<el-input
|
|
|
v-model="form.password"
|
|
|
type="password"
|
|
|
show-password
|
|
|
maxlength="255"
|
|
|
autocomplete="new-password"
|
|
|
:placeholder="editMode === 'create' ? '必填' : '留空表示不修改'"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
<el-button @click="editDialog = false">取消</el-button>
|
|
|
<el-button type="primary" :loading="saving" @click="submitEdit">确定</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
|
.proto-reviewer-page {
|
|
|
--bg: #f5f3f4;
|
|
|
--text: #1f1f1f;
|
|
|
--primary: #b40010;
|
|
|
--primary-soft: #fbe9eb;
|
|
|
--shadow-soft: 0 4px 14px rgba(46, 24, 26, 0.06);
|
|
|
}
|
|
|
|
|
|
.rev-head {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
flex-wrap: wrap;
|
|
|
gap: 0.5rem;
|
|
|
margin-bottom: 0.75rem;
|
|
|
}
|
|
|
|
|
|
.section-title {
|
|
|
font-size: 1.35rem;
|
|
|
font-weight: 620;
|
|
|
color: #2f2a2b;
|
|
|
}
|
|
|
|
|
|
.rev-alert {
|
|
|
margin-bottom: 0.75rem;
|
|
|
}
|
|
|
|
|
|
.reviewer-manage-shell {
|
|
|
display: grid;
|
|
|
grid-template-columns: 14.5rem minmax(0, 1fr);
|
|
|
gap: 0.9rem;
|
|
|
align-items: start;
|
|
|
}
|
|
|
|
|
|
.reviewer-track-panel,
|
|
|
.reviewer-editor-panel {
|
|
|
background: #fff;
|
|
|
border: 1px solid #ece2e4;
|
|
|
border-radius: 10px;
|
|
|
box-shadow: var(--shadow-soft);
|
|
|
}
|
|
|
|
|
|
.reviewer-track-panel {
|
|
|
padding: 0.72rem;
|
|
|
}
|
|
|
|
|
|
.reviewer-track-panel-title {
|
|
|
color: #827579;
|
|
|
font-size: 0.78rem;
|
|
|
font-weight: 600;
|
|
|
margin-bottom: 0.55rem;
|
|
|
}
|
|
|
|
|
|
.reviewer-track-tabs {
|
|
|
display: grid;
|
|
|
gap: 0.42rem;
|
|
|
}
|
|
|
|
|
|
.reviewer-track-tab {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
gap: 0.5rem;
|
|
|
width: 100%;
|
|
|
border: 1px solid transparent;
|
|
|
border-radius: 8px;
|
|
|
background: #fbf7f8;
|
|
|
color: #3e3437;
|
|
|
padding: 0.58rem 0.62rem;
|
|
|
text-align: left;
|
|
|
cursor: pointer;
|
|
|
font: inherit;
|
|
|
}
|
|
|
|
|
|
.reviewer-track-tab.active {
|
|
|
color: var(--primary);
|
|
|
border-color: #e5bdc1;
|
|
|
background: var(--primary-soft);
|
|
|
}
|
|
|
|
|
|
.reviewer-track-tab-name {
|
|
|
font-size: 0.86rem;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.reviewer-track-tab-count {
|
|
|
color: #8b7e81;
|
|
|
font-size: 0.78rem;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.reviewer-editor-panel {
|
|
|
padding: 0.8rem;
|
|
|
}
|
|
|
|
|
|
.reviewer-editor-head {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: flex-start;
|
|
|
gap: 0.75rem;
|
|
|
margin-bottom: 0.72rem;
|
|
|
}
|
|
|
|
|
|
.reviewer-editor-actions {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 0.45rem;
|
|
|
flex-wrap: wrap;
|
|
|
justify-content: flex-end;
|
|
|
}
|
|
|
|
|
|
.reviewer-track-title {
|
|
|
margin: 0 0 0.18rem;
|
|
|
font-size: 1rem;
|
|
|
color: #322b2d;
|
|
|
font-weight: 650;
|
|
|
}
|
|
|
|
|
|
.reviewer-track-meta {
|
|
|
color: #8b7e81;
|
|
|
font-size: 0.78rem;
|
|
|
}
|
|
|
|
|
|
.table-responsive {
|
|
|
overflow-x: auto;
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
}
|
|
|
|
|
|
.reviewer-manage-table {
|
|
|
min-width: 620px;
|
|
|
width: 100%;
|
|
|
border-collapse: collapse;
|
|
|
}
|
|
|
|
|
|
.reviewer-manage-table th {
|
|
|
color: #4a4043;
|
|
|
background: #faf7f7;
|
|
|
font-size: 0.8rem;
|
|
|
font-weight: 600;
|
|
|
padding: 0.65rem 0.62rem;
|
|
|
border-bottom: 1px solid #ece2e4;
|
|
|
}
|
|
|
|
|
|
.reviewer-manage-table td {
|
|
|
padding: 0.65rem 0.62rem;
|
|
|
border-bottom: 1px solid #f0e8ea;
|
|
|
font-size: 0.88rem;
|
|
|
vertical-align: middle;
|
|
|
}
|
|
|
|
|
|
.reviewer-row-actions {
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.reviewer-row-actions .prm-btn-outline + .prm-btn-light {
|
|
|
margin-left: 0.35rem;
|
|
|
}
|
|
|
|
|
|
.prm-btn-outline,
|
|
|
.prm-btn-light {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
font-size: 0.8rem;
|
|
|
border-radius: 6px;
|
|
|
padding: 0.22rem 0.55rem;
|
|
|
cursor: pointer;
|
|
|
font-weight: 500;
|
|
|
border: 1px solid #c9b8bb;
|
|
|
background: #fff;
|
|
|
color: var(--primary);
|
|
|
}
|
|
|
|
|
|
.prm-btn-outline:hover:not(:disabled) {
|
|
|
background: var(--primary-soft);
|
|
|
}
|
|
|
|
|
|
.prm-btn-outline:disabled {
|
|
|
opacity: 0.45;
|
|
|
cursor: not-allowed;
|
|
|
}
|
|
|
|
|
|
.prm-btn-outline.sm,
|
|
|
.prm-btn-light.sm {
|
|
|
padding: 0.18rem 0.45rem;
|
|
|
font-size: 0.78rem;
|
|
|
}
|
|
|
|
|
|
.prm-btn-light {
|
|
|
color: #3e3437;
|
|
|
background: #fafafa;
|
|
|
border-color: #ddd5d7;
|
|
|
}
|
|
|
|
|
|
.prm-btn-light:hover {
|
|
|
background: #f3f0f1;
|
|
|
}
|
|
|
|
|
|
.empty-hint {
|
|
|
padding: 1rem;
|
|
|
color: #8b7e81;
|
|
|
font-size: 0.86rem;
|
|
|
}
|
|
|
|
|
|
.footnote {
|
|
|
margin: 0.55rem 0 0;
|
|
|
font-size: 0.72rem;
|
|
|
color: #8b7e81;
|
|
|
}
|
|
|
|
|
|
@media (max-width: 992px) {
|
|
|
.admin-desktop-page .reviewer-manage-shell {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
.admin-desktop-page .reviewer-track-tabs {
|
|
|
display: flex;
|
|
|
gap: 0.5rem;
|
|
|
overflow-x: auto;
|
|
|
padding-bottom: 0.1rem;
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
}
|
|
|
|
|
|
.admin-desktop-page .reviewer-track-tab {
|
|
|
flex: 0 0 min(13rem, 72vw);
|
|
|
}
|
|
|
|
|
|
.reviewer-editor-head {
|
|
|
flex-direction: column;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
|
|
|
.reviewer-editor-actions {
|
|
|
justify-content: flex-start;
|
|
|
}
|
|
|
}
|
|
|
</style>
|