Files
reader-star/apps/admin/src/views/template/template-detail/index.vue
rjl 7d49143ae0 feat(admin): 新增UUID生成工具并优化模板管理
- 新增 uuid.ts 工具函数,提供原生和兼容性 UUID 生成方案
- 更新模板详情页面,使用 generateUUID() 替代 crypto.randomUUID()
- 模板列表新增页码字段显示
- 优化模板列表组件代码格式,改进可读性
- 更新 Git 提交消息规范文档,强制使用中文描述和具体要求
2026-03-19 11:16:01 +08:00

292 lines
7.8 KiB
Vue

<!-- eslint-disable no-console -->
<script lang="ts" setup>
import type { GenSettings, Region, TemplateInfo } from './types'
import { Icon } from '@iconify/vue'
import { NButton, NUpload, useMessage } from 'naive-ui'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { useRouterPush } from '@/hooks/common/router'
import { fetchAddTemplate, fetchTemplateDetail } from '@/service/api/template'
import { generateUUID } from '@/utils/uuid'
import { useUpload } from './hooks/useUpload'
import TemplateCanvas from './modules/TemplateCanvas.vue'
import TemplateSettings from './modules/TemplateSettings.vue'
const route = useRoute()
const message = useMessage()
const { routerBack } = useRouterPush()
// 模版基础信息
const templateInfo = ref<TemplateInfo>({
name: '',
width: 2100,
height: 2997,
})
const imgSrc = ref('')
// 生成配置
const genSettings = ref<GenSettings>({
cols: 6,
rows: 1,
w: 137,
h: 133,
gapX: 20,
gapY: 0,
startX: 0,
startY: 0,
})
// 区域列表
const regions = ref<Region[]>([])
// 选中区域集合
const selectedRegionIds = ref<Set<string>>(new Set())
// 视图控制
const showGrid = ref(false)// 是否显示网格
const showRuler = ref(true)// 是否显示标尺
const showCoordinates = ref(false)// 是否显示坐标
// Use Hook
const { loading, handleUpload, handleUploadCheck } = useUpload(imgSrc, templateInfo, regions, selectedRegionIds)
const uploadRef = ref<any>(null)
function handleUploadTrigger() {
handleUploadCheck(() => {
uploadRef.value?.openOpenFileDialog()
})
}
// --- Actions ---
/**
* 处理区域生成
*/
function handleGenerate() {
const groupId = generateUUID()
const newRegions: Region[] = []
// 计算起始编号:查找现有的最大 Qx 编号
let maxNum = 0
regions.value.forEach((r) => {
const match = r.label.match(/^Q(\d+)$/)
if (match) {
const n = Number.parseInt(match[1], 10)
if (!Number.isNaN(n) && n > maxNum) {
maxNum = n
}
}
})
const currentX = genSettings.value.startX
const currentY = genSettings.value.startY
for (let r = 0; r < genSettings.value.rows; r++) {
for (let c = 0; c < genSettings.value.cols; c++) {
const index = maxNum + r * genSettings.value.cols + c + 1 // 计算当前区域的编号
newRegions.push({
id: generateUUID(),
groupId,
x: currentX + c * (genSettings.value.w + genSettings.value.gapX),
y: currentY + r * (genSettings.value.h + genSettings.value.gapY),
w: genSettings.value.w,
h: genSettings.value.h,
label: `Q${index}`, // 题目编号递增
selected: false,
})
}
}
regions.value.push(...newRegions)
if (newRegions.length > 1) {
const newIds = new Set(newRegions.map(r => r.id))
selectedRegionIds.value = newIds
}
window?.$message?.success(`已生成 ${newRegions.length} 个区域`)
}
/**
* 删除区域
*/
function deleteRegion(id: string) {
regions.value = regions.value.filter(r => r.id !== id)
selectedRegionIds.value.delete(id)
}
/**
* 清空所有
*/
function clearAll() {
regions.value = []
selectedRegionIds.value.clear()
}
/**
* 保存模板
*/
async function handleSave() {
const id = Number(route.query.id) || 0
const params: Api.Template.AddTemplateParams = {
id,
pageNo: templateInfo.value.pageNo || '1761.172.8.25',
name: templateInfo.value.name,
backGroundUrl: templateInfo.value.backGroundUrl || '',
width: Math.round(templateInfo.value.width),
height: Math.round(templateInfo.value.height),
tempContent: JSON.stringify(regions.value),
}
console.log(params, 'params')
// 校验
if (!params.name) {
message.error('请输入模板名称')
return
}
if (!params.pageNo) {
message.error('请输入页码')
return
}
const { error } = await fetchAddTemplate(params)
if (!error) {
message.success('保存成功')
routerBack()
}
}
/**
* 详情编辑
* 初始化数据
*/
async function initData() {
const id = Number(route.query.id)
if (id) {
try {
loading.value = true
const { data: response, error } = await fetchTemplateDetail(id)
console.log(response, 'response')
console.log(error, 'error')
if (!error && response) {
const { data, success } = response
if (success) {
const { Name, Width, Height, BackGroundUrl, TempContent, PageNo } = data || {}
templateInfo.value = {
name: Name,
width: Width,
height: Height,
pageNo: PageNo,
backGroundUrl: BackGroundUrl,
}
imgSrc.value = BackGroundUrl || ''
if (TempContent) {
try {
regions.value = JSON.parse(TempContent || '[]')
}
catch (e) {
console.error('Failed to parse tempContent', e)
}
}
}
}
}
catch (e) {
console.error('Failed to fetch template detail', e)
}
finally {
loading.value = false
}
}
}
onMounted(() => {
initData()
})
</script>
<template>
<div class="h-full flex flex-col bg-gray-100">
<!-- Header -->
<div class="h-14 flex shrink-0 items-center justify-between border-b bg-white px-4">
<div class="flex items-center gap-2">
<NButton quaternary circle @click="routerBack()">
<template #icon>
<Icon icon="carbon:arrow-left" />
</template>
</NButton>
<span class="text-lg font-medium">答题卡排版编辑器 V2.0</span>
</div>
<div class="flex items-center gap-3">
<NButton
size="small"
:type="showGrid ? 'primary' : 'default'"
secondary
@click="showGrid = !showGrid"
>
<template #icon>
<Icon icon="carbon:grid" />
</template>
{{ showGrid ? '隐藏网格' : '显示网格' }}
</NButton>
<NButton
size="small"
:type="showRuler ? 'primary' : 'default'"
secondary
@click="showRuler = !showRuler"
>
<template #icon>
<Icon icon="carbon:ruler" />
</template>
{{ showRuler ? '隐藏标尺' : '显示标尺' }}
</NButton>
<NButton
size="small"
:type="showCoordinates ? 'primary' : 'default'"
secondary
@click="showCoordinates = !showCoordinates"
>
<template #icon>
<Icon icon="carbon:center-to-fit" />
</template>
{{ showCoordinates ? '关闭测距' : '开启测距' }}
</NButton>
<div class="h-6 w-px bg-gray-200" />
<NUpload
ref="uploadRef" :show-file-list="false" :custom-request="handleUpload"
accept=".pdf,image/png,image/jpeg,image/jpg" style="display: none"
/>
<NButton @click="handleUploadTrigger">
<template #icon>
<Icon icon="carbon:upload" />
</template>
上传底稿
</NButton>
<NButton type="primary" :loading="loading" :disabled="!templateInfo.backGroundUrl" @click="handleSave">
完成设计并保存
</NButton>
</div>
</div>
<div class="flex flex-1 overflow-hidden">
<!-- Left: Canvas Area -->
<TemplateCanvas
v-model:template-info="templateInfo" v-model:regions="regions"
v-model:selected-region-ids="selectedRegionIds" :img-src="imgSrc" :loading="loading"
:show-grid="showGrid" :show-ruler="showRuler" :show-coordinates="showCoordinates"
/>
<!-- Right: Settings Panel -->
<TemplateSettings
v-model:template-info="templateInfo" v-model:gen-settings="genSettings" v-model:selected-region-ids="selectedRegionIds"
:regions="regions"
@generate="handleGenerate" @delete-region="deleteRegion" @clear-all="clearAll"
/>
</div>
</div>
</template>