1
This commit is contained in:
+12
-10
@@ -1,15 +1,22 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
List as ListIcon,
|
||||
Key as KeyIcon,
|
||||
Refresh as RefreshIcon,
|
||||
SwitchButton as SwitchIcon,
|
||||
Picture as PictureIcon,
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 详情页 /datasets/:id 仍高亮「数据训练」菜单项
|
||||
const activeMenu = computed(() =>
|
||||
route.path.startsWith('/datasets') ? '/datasets' : route.path,
|
||||
)
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('adminToken')
|
||||
router.push('/login')
|
||||
@@ -22,8 +29,11 @@ function logout() {
|
||||
|
||||
<el-container v-else class="app-layout">
|
||||
<el-aside width="200px" class="app-aside">
|
||||
<div class="app-logo">视野管理端</div>
|
||||
<el-menu :default-active="route.path" router>
|
||||
<el-menu :default-active="activeMenu" router>
|
||||
<el-menu-item index="/datasets">
|
||||
<el-icon><PictureIcon /></el-icon>
|
||||
<span>数据训练</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/orders">
|
||||
<el-icon><ListIcon /></el-icon>
|
||||
<span>订单管理</span>
|
||||
@@ -57,14 +67,6 @@ function logout() {
|
||||
.app-aside {
|
||||
background: #001529;
|
||||
}
|
||||
.app-logo {
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.app-aside :deep(.el-menu) {
|
||||
border-right: none;
|
||||
background: #001529;
|
||||
|
||||
@@ -3,13 +3,17 @@ import Login from '../views/Login.vue'
|
||||
import Orders from '../views/Orders.vue'
|
||||
import Licenses from '../views/Licenses.vue'
|
||||
import AppVersions from '../views/AppVersions.vue'
|
||||
import Datasets from '../views/Datasets.vue'
|
||||
import DatasetDetail from '../views/DatasetDetail.vue'
|
||||
|
||||
// 生产部署于 /admin/ 前缀(后端静态托管),history 路由由后端 SPA fallback 兜底
|
||||
const router = createRouter({
|
||||
history: createWebHistory('/admin/'),
|
||||
routes: [
|
||||
{ path: '/login', name: 'login', component: Login, meta: { title: '登录' } },
|
||||
{ path: '/', redirect: '/orders' },
|
||||
{ path: '/', redirect: '/datasets' },
|
||||
{ path: '/datasets', name: 'datasets', component: Datasets, meta: { title: '数据训练' } },
|
||||
{ path: '/datasets/:id', name: 'datasetDetail', component: DatasetDetail, meta: { title: '数据训练详情' } },
|
||||
{ path: '/orders', name: 'orders', component: Orders, meta: { title: '订单管理' } },
|
||||
{ path: '/licenses', name: 'licenses', component: Licenses, meta: { title: '授权管理' } },
|
||||
{ path: '/app-versions', name: 'appVersions', component: AppVersions, meta: { title: '版本管理' } },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,565 @@
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Picture, Setting, VideoPlay, Download } from '@element-plus/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import request from '../api/request'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const size = ref(12)
|
||||
const keyword = ref('')
|
||||
|
||||
const createVisible = ref(false)
|
||||
const createForm = reactive({ name: '', source: 'manual' })
|
||||
const creating = ref(false)
|
||||
|
||||
const configVisible = ref(false)
|
||||
const configSaving = ref(false)
|
||||
const configForm = reactive({
|
||||
id: 0,
|
||||
description: '',
|
||||
cover: '',
|
||||
})
|
||||
const coverFile = ref(null) // 新选择的封面文件(el-upload 单文件)
|
||||
const coverFileList = ref([]) // 封面回显:已有封面(服务端 url)或新选文件(本地预览)
|
||||
const coverDeleted = ref(false) // 用户删除了已有封面(保存时调删除接口)
|
||||
|
||||
const trainVisible = ref(false)
|
||||
const training = ref(false)
|
||||
const trainForm = reactive({ datasetId: 0, name: '', imgsz: 704, epochs: 150, batch: 16, device: '0' })
|
||||
|
||||
const statusMap = { building: '建设中', labeled: '已标注', synced: '已同步' }
|
||||
const statusTag = { building: 'info', labeled: 'success', synced: 'primary' }
|
||||
const trainStatusMap = { running: '训练中', success: '已训练', failed: '训练失败' }
|
||||
const trainStatusTag = { running: 'primary', success: 'success', failed: 'danger' }
|
||||
|
||||
function load() {
|
||||
loading.value = true
|
||||
request
|
||||
.get('/datasets', { params: { page: page.value, size: size.value, keyword: keyword.value || undefined } })
|
||||
.then((data) => {
|
||||
list.value = data.list || []
|
||||
total.value = data.total || 0
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function search() {
|
||||
page.value = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
createForm.name = ''
|
||||
createForm.source = 'manual'
|
||||
createVisible.value = true
|
||||
}
|
||||
|
||||
function submitCreate() {
|
||||
if (!createForm.name.trim()) {
|
||||
ElMessage.warning('请输入数据集名称')
|
||||
return
|
||||
}
|
||||
creating.value = true
|
||||
request
|
||||
.post('/datasets', { name: createForm.name.trim(), source: createForm.source })
|
||||
.then(() => {
|
||||
ElMessage.success('数据集已创建,请上传或生成图片')
|
||||
createVisible.value = false
|
||||
page.value = 1
|
||||
load()
|
||||
})
|
||||
.finally(() => {
|
||||
creating.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// ---------- 数据集级配置(描述 + 封面;AI 端点/训练机 SSH 走 config.yml,与数据集无关) ----------
|
||||
|
||||
function openConfig(row) {
|
||||
Object.assign(configForm, {
|
||||
id: row.id,
|
||||
description: row.description || '',
|
||||
cover: row.cover || '',
|
||||
})
|
||||
coverFile.value = null
|
||||
coverDeleted.value = false
|
||||
// 已有封面回显:el-upload picture-card 直接以 url 项展示
|
||||
coverFileList.value = row.cover ? [{ name: '当前封面', url: imgUrl(`/api/v1/admin/datasets/cover?datasetId=${row.id}`) }] : []
|
||||
configVisible.value = true
|
||||
}
|
||||
|
||||
// 封面文件选中:多选时只保留最后一个(替换语义);新文件选中即视为不再删除
|
||||
function onCoverChange(file, fileList) {
|
||||
if (fileList.length > 1) coverFileList.value = [fileList[fileList.length - 1]]
|
||||
coverFile.value = coverFileList.value.length ? coverFileList.value[0].raw : null
|
||||
if (coverFile.value) coverDeleted.value = false
|
||||
}
|
||||
|
||||
// 删除项:删服务端已有封面(无 raw)→ 标记待删除;删新选文件且列表清空 → 同样按删除处理(服务端旧封面仍在)
|
||||
function onCoverRemove(file) {
|
||||
coverFile.value = null
|
||||
if (!file.raw) {
|
||||
coverDeleted.value = true
|
||||
} else if (!coverFileList.value.length && configForm.cover) {
|
||||
coverDeleted.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function submitConfig() {
|
||||
configSaving.value = true
|
||||
try {
|
||||
if (coverFile.value) {
|
||||
const fd = new FormData()
|
||||
fd.append('datasetId', configForm.id)
|
||||
fd.append('file', coverFile.value)
|
||||
await request.post('/datasets/cover', fd, { timeout: 60000 })
|
||||
} else if (coverDeleted.value) {
|
||||
await request.post('/datasets/cover/delete', { datasetId: configForm.id })
|
||||
}
|
||||
// cover 由上传接口单独维护,update 不携带
|
||||
await request.post('/datasets/update', { ...configForm, cover: '' })
|
||||
ElMessage.success('配置已保存')
|
||||
configVisible.value = false
|
||||
load()
|
||||
} finally {
|
||||
configSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 开始训练 ----------
|
||||
|
||||
function openTrain(row) {
|
||||
trainForm.datasetId = row.id
|
||||
trainForm.name = `${row.name} 训练`
|
||||
trainForm.imgsz = 704
|
||||
trainForm.epochs = 150
|
||||
trainForm.batch = 16
|
||||
trainForm.device = '0'
|
||||
trainVisible.value = true
|
||||
}
|
||||
|
||||
function submitTrain() {
|
||||
if (!trainForm.name.trim()) {
|
||||
ElMessage.warning('请输入任务名称')
|
||||
return
|
||||
}
|
||||
training.value = true
|
||||
request
|
||||
.post('/trainings', { ...trainForm })
|
||||
.then(() => {
|
||||
ElMessage.success('训练任务已发起,完成后可发布为模型版本')
|
||||
trainVisible.value = false
|
||||
load()
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
training.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// ---------- 导出 / 删除 ----------
|
||||
|
||||
function exportDataset(row) {
|
||||
window.open(`${location.origin}/api/v1/admin/datasets/export?datasetId=${row.id}`, '_blank')
|
||||
}
|
||||
|
||||
function removeDataset(row) {
|
||||
const warn =
|
||||
row.source === 'ai' || row.imageCount > 0
|
||||
? '将删除图片文件、标注与记录,AI 生成图属付费资产,删除后无法恢复。'
|
||||
: ''
|
||||
ElMessageBox.confirm(`确定删除数据集「${row.name}」?${warn}`, '删除数据集', {
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => request.post('/datasets/delete', { id: row.id }))
|
||||
.then(() => {
|
||||
ElMessage.success('已删除')
|
||||
load()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// <img> 标签无法带自定义请求头,图片地址以 query 参数携带 admin token
|
||||
function imgUrl(u) {
|
||||
return `${location.origin}${u}&token=${encodeURIComponent(localStorage.getItem('adminToken') || '')}`
|
||||
}
|
||||
|
||||
function coverUrl(row) {
|
||||
if (!row.cover) return ''
|
||||
return imgUrl(`/api/v1/admin/datasets/cover?datasetId=${row.id}`)
|
||||
}
|
||||
|
||||
// 训练进度(百分比):有总轮数才显示比例
|
||||
function trainPercent(row) {
|
||||
if (row.trainingStatus !== 'running' || !row.trainingTotalEpochs) return 0
|
||||
return Math.min(100, Math.round((row.trainingCurrentEpoch / row.trainingTotalEpochs) * 100))
|
||||
}
|
||||
|
||||
function publishModel(row) {
|
||||
ElMessageBox.confirm(
|
||||
`确定将「${row.name}」的最新训练结果发布为模型版本?将按数据集版本号自增(m<主>.<次>.<修订>),客户端下次启动热更新下载。`,
|
||||
'发布模型版本',
|
||||
{ confirmButtonText: '发布', cancelButtonText: '取消', type: 'warning' },
|
||||
)
|
||||
.then(() => request.post('/trainings/publish', { id: row.trainingId }))
|
||||
.then((data) => {
|
||||
ElMessage.success(`已发布版本 ${data.version}`)
|
||||
load()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// 有训练中的任务时周期刷新,卡片进度条保持最新
|
||||
let trainPoll = null
|
||||
onMounted(() => {
|
||||
load()
|
||||
trainPoll = setInterval(() => {
|
||||
if (list.value.some((r) => r.trainingStatus === 'running')) load()
|
||||
}, 10000)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (trainPoll) clearInterval(trainPoll)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
class="kw"
|
||||
placeholder="按名称搜索"
|
||||
clearable
|
||||
@keyup.enter="search"
|
||||
@clear="search"
|
||||
/>
|
||||
<el-button @click="search">搜索</el-button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button type="primary" :icon="Plus" @click="openCreate">新建数据集</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="ds-grid">
|
||||
<div v-for="row in list" :key="row.id" class="ds-card" @click="router.push(`/datasets/${row.id}`)">
|
||||
<div class="ds-cover">
|
||||
<img v-if="row.cover" :src="coverUrl(row)" :alt="row.name" loading="lazy" />
|
||||
<div v-else class="ds-cover-placeholder"><el-icon :size="34"><Picture /></el-icon></div>
|
||||
<div v-if="row.source === 'ai'" class="ds-cover-source">AI 生成</div>
|
||||
</div>
|
||||
<div class="ds-body">
|
||||
<div class="ds-name" :title="row.name">{{ row.name }}</div>
|
||||
<div class="ds-desc">{{ row.description || '暂无描述,点击进入管理图片与标注' }}</div>
|
||||
<div class="ds-stats">
|
||||
<span>图片 {{ row.imageCount }}</span>
|
||||
<span>已标注 {{ row.labeledCount }}</span>
|
||||
<el-tag :type="statusTag[row.status] || 'info'" size="small">{{ statusMap[row.status] || row.status }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ds-actions" @click.stop>
|
||||
<el-button type="primary" size="small" :icon="VideoPlay" :disabled="row.trainingStatus === 'running'" @click="openTrain(row)">
|
||||
{{ row.trainingStatus === 'running' ? '训练中' : '开始训练' }}
|
||||
</el-button>
|
||||
<el-button size="small" :icon="Setting" @click="openConfig(row)">配置</el-button>
|
||||
<el-button size="small" :icon="Download" @click="exportDataset(row)">导出</el-button>
|
||||
<el-button size="small" type="danger" @click="removeDataset(row)">删除</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 训练任务进度与状态(最新一条训练记录) -->
|
||||
<div v-if="row.trainingStatus" class="ds-train" @click.stop>
|
||||
<template v-if="row.trainingStatus === 'running'">
|
||||
<el-progress :percentage="trainPercent(row)" :stroke-width="6" :show-text="false" class="ds-train-bar" />
|
||||
<div class="ds-train-text">
|
||||
<span>训练中 {{ row.trainingCurrentEpoch || 0 }}/{{ row.trainingTotalEpochs || '-' }} 轮</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag :type="trainStatusTag[row.trainingStatus] || 'info'" size="small">
|
||||
{{ trainStatusMap[row.trainingStatus] || row.trainingStatus }}
|
||||
</el-tag>
|
||||
<el-button v-if="row.trainingStatus === 'success' && row.trainingId" size="small" type="success" @click="publishModel(row)">
|
||||
发布模型
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!loading && !list.length" description="暂无数据集,点击右上角新建" />
|
||||
</div>
|
||||
|
||||
<el-pagination
|
||||
class="pager"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="size"
|
||||
:total="total"
|
||||
:page-sizes="[12, 24, 48]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@change="load"
|
||||
/>
|
||||
|
||||
<!-- 新建数据集 -->
|
||||
<el-dialog v-model="createVisible" title="新建数据集" width="min(440px, 92vw)">
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="名称">
|
||||
<el-input
|
||||
v-model="createForm.name"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
placeholder="中文/字母/数字/下划线/短横线,唯一且作磁盘目录名"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片来源">
|
||||
<el-radio-group v-model="createForm.source">
|
||||
<el-radio value="manual">手动上传</el-radio>
|
||||
<el-radio value="ai">AI 生成</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="submitCreate">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 数据集配置 -->
|
||||
<el-dialog v-model="configVisible" title="数据集配置" width="min(720px, 94vw)">
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="configForm.description" type="textarea" :rows="6" maxlength="500" show-word-limit placeholder="数据集说明,展示在卡片上" />
|
||||
</el-form-item>
|
||||
<el-form-item label="封面">
|
||||
<div class="cover-row">
|
||||
<el-upload
|
||||
v-model:file-list="coverFileList"
|
||||
:auto-upload="false"
|
||||
accept=".jpg,.jpeg,.png"
|
||||
list-type="picture-card"
|
||||
:on-change="onCoverChange"
|
||||
:on-remove="onCoverRemove"
|
||||
>
|
||||
<div v-if="coverFileList.length === 0" class="upload-tile">
|
||||
<el-icon :size="22"><Plus /></el-icon>
|
||||
</div>
|
||||
</el-upload>
|
||||
<span class="add-tip">jpg/jpeg/png,≤2MB;悬停预览右上角 × 可删除当前封面</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="configVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="configSaving" @click="submitConfig">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 开始训练 -->
|
||||
<el-dialog v-model="trainVisible" title="开始训练" width="min(480px, 92vw)">
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="任务名称">
|
||||
<el-input v-model="trainForm.name" maxlength="50" show-word-limit placeholder="如:雉鸡数据集 v2 训练" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分辨率">
|
||||
<el-input-number v-model="trainForm.imgsz" :min="64" :max="2048" :step="32" />
|
||||
<span class="field-tip">与端侧推理对齐,默认 704</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="轮数">
|
||||
<el-input-number v-model="trainForm.epochs" :min="1" :max="1000" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Batch">
|
||||
<el-input-number v-model="trainForm.batch" :min="1" :max="128" />
|
||||
</el-form-item>
|
||||
<el-form-item label="设备">
|
||||
<el-input v-model="trainForm.device" maxlength="16" placeholder="GPU 编号 0,CPU 填 cpu" />
|
||||
</el-form-item>
|
||||
<div class="start-tip">训练前自动整理 80/20 训练/验证集,须先完成图片标注;训练机并发度 1,已有运行中任务会被拒绝。</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="trainVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="training" @click="submitTrain">发起训练</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
gap: 10px;
|
||||
}
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.kw {
|
||||
width: 220px;
|
||||
}
|
||||
.pager {
|
||||
margin-top: 14px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.ds-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 14px;
|
||||
min-height: 120px;
|
||||
}
|
||||
.ds-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.ds-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.ds-cover {
|
||||
position: relative;
|
||||
height: 150px;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
.ds-cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
.ds-cover-placeholder {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
.ds-cover-source {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.ds-train {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px 12px;
|
||||
border-top: 1px dashed #ebeef5;
|
||||
}
|
||||
.ds-train-bar {
|
||||
flex: 1;
|
||||
}
|
||||
.ds-train-text {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ds-body {
|
||||
padding: 10px 12px 6px;
|
||||
}
|
||||
.ds-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ds-desc {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.ds-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.ds-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 8px 12px 12px;
|
||||
border-top: 1px dashed #ebeef5;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.field-tip {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
.start-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
margin-left: 90px;
|
||||
}
|
||||
.cover-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
/* 上传方块:虚线边框 + 居中加号(el-upload picture-card 触发块) */
|
||||
.upload-tile {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8c939d;
|
||||
}
|
||||
.add-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
align-self: flex-start;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.toolbar {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.toolbar-left {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
.toolbar-right {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.kw {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
}
|
||||
.ds-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -23,7 +23,7 @@ function login() {
|
||||
.then(() => {
|
||||
localStorage.setItem('adminToken', token.value.trim())
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/orders')
|
||||
router.push('/datasets')
|
||||
})
|
||||
.catch(() => {
|
||||
errorMsg.value = 'token 无效或服务端未配置(检查 config.yml admin.token)'
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://127.0.0.1:8080',
|
||||
'/api': 'http://127.0.0.1:18080',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user