feat(template): 增强答题卡编辑器功能并优化类型定义
- 新增模板详情编辑功能,支持从后端加载并回填数据 - 在画布中增加网格、标尺和坐标显示辅助工具 - 优化模板列表操作按钮布局,新增打印功能 - 扩展响应类型定义,增加 success 字段和 TemplateDetail 接口 - 添加 ESLint 配置排除 Markdown 文件,并完善事件总线类型 - 新增请求工具使用文档,说明错误处理机制
This commit is contained in:
@ -12,7 +12,7 @@ export function fetchTemplateList(data?: Api.Template.TemplateSearchParams) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增模板
|
* 新增模板, 更新模板
|
||||||
*/
|
*/
|
||||||
export function fetchAddTemplate(data?: Api.Template.AddTemplateParams) {
|
export function fetchAddTemplate(data?: Api.Template.AddTemplateParams) {
|
||||||
return request<App.Service.Response<Api.Common.PaginatingQueryRecord<Api.Template.CommonRecord>>>({
|
return request<App.Service.Response<Api.Common.PaginatingQueryRecord<Api.Template.CommonRecord>>>({
|
||||||
@ -26,7 +26,7 @@ export function fetchAddTemplate(data?: Api.Template.AddTemplateParams) {
|
|||||||
* 获取模板详情
|
* 获取模板详情
|
||||||
*/
|
*/
|
||||||
export function fetchTemplateDetail(id: number) {
|
export function fetchTemplateDetail(id: number) {
|
||||||
return request<App.Service.Response<Api.Common.PaginatingQueryRecord<Api.Template.CommonRecord>>>({
|
return request<App.Service.Response<Api.Template.TemplateDetail>>({
|
||||||
url: `/Base/ActivityMain/GetTemplete_PagByID?id=${id}`,
|
url: `/Base/ActivityMain/GetTemplete_PagByID?id=${id}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
})
|
})
|
||||||
|
|||||||
76
apps/admin/src/service/request/readme.md
Normal file
76
apps/admin/src/service/request/readme.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
## error 字段 不是后端接口直接返回的 ,而是前端请求工具 request (基于 @sa/axios 的 createFlatRequest 封装) 为了简化错误处理而增加的。
|
||||||
|
|
||||||
|
它的机制如下:
|
||||||
|
|
||||||
|
- 请求成功 : error 为 null , data 为后端返回的响应体(包含 code , msg , data , success 等字段)。
|
||||||
|
- 请求失败 (如网络断开、超时): error 会包含错误信息对象, data 为 null 。
|
||||||
|
这种写法让您可以直接解构返回值判断请求是否成功,避免了使用 `try-catch` 包裹。
|
||||||
|
|
||||||
|
总结:
|
||||||
|
|
||||||
|
- response (您重命名的 data ) :包含后端返回的 { success, code, msg, data } 。
|
||||||
|
- error :前端请求库生成的错误对象,用于判断 HTTP 请求层面的成败
|
||||||
|
|
||||||
|
参考:
|
||||||
|
|
||||||
|
- [@sa/axios 文档](https://bytedance.larkoffice.com/wiki/wikcnn33hQ3h3FkqCnqCn33h3Fk)
|
||||||
|
|
||||||
|
例子:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import request from '@/service/request'
|
||||||
|
|
||||||
|
// 成功请求
|
||||||
|
const { data, error } = await request.get('/api/success')
|
||||||
|
if (error) {
|
||||||
|
console.error('请求失败:', error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('请求成功:', data)
|
||||||
|
|
||||||
|
// 失败请求(如网络断开)
|
||||||
|
const { data: failedData, error: failedError } = await request.get('/api/fail')
|
||||||
|
if (failedError) {
|
||||||
|
console.error('请求失败:', failedError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('请求成功:', failedData) // 不会执行到这里
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化模板详情
|
||||||
|
*/
|
||||||
|
async function initData() {
|
||||||
|
const id = Number(route.query.id)
|
||||||
|
if (id) {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const { data: response, error } = await fetchTemplateDetail(id)
|
||||||
|
if (!error && response) {
|
||||||
|
const { data, success } = response
|
||||||
|
if (success) {
|
||||||
|
const { Name, Width, Height, BackGroundUrl, TempContent } = data || {}
|
||||||
|
templateInfo.value = {
|
||||||
|
name: Name,
|
||||||
|
width: Width,
|
||||||
|
height: Height,
|
||||||
|
backGroundUrl: BackGroundUrl,
|
||||||
|
}
|
||||||
|
imgSrc.value = BackGroundUrl || ''
|
||||||
|
|
||||||
|
if (TempContent) {
|
||||||
|
try {
|
||||||
|
regions.value = JSON.parse(TempContent || '[]')
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('Failed to parse tempContent', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
18
apps/admin/src/typings/api/template.d.ts
vendored
18
apps/admin/src/typings/api/template.d.ts
vendored
@ -32,6 +32,24 @@ declare namespace Api {
|
|||||||
tempContent: string
|
tempContent: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** template detail */
|
||||||
|
interface TemplateDetail {
|
||||||
|
/** template id */
|
||||||
|
ID: number
|
||||||
|
/** template name */
|
||||||
|
Name: string
|
||||||
|
/** width */
|
||||||
|
Width: number
|
||||||
|
/** height */
|
||||||
|
Height: number
|
||||||
|
/** background url */
|
||||||
|
BackGroundUrl: string
|
||||||
|
/** template content */
|
||||||
|
TempContent: string
|
||||||
|
/** competition id */
|
||||||
|
CompetitionId?: number
|
||||||
|
}
|
||||||
|
|
||||||
/** template search params */
|
/** template search params */
|
||||||
interface TemplateSearchParams extends PaginatingCommonParams {
|
interface TemplateSearchParams extends PaginatingCommonParams {
|
||||||
/** competition activity name */
|
/** competition activity name */
|
||||||
|
|||||||
4
apps/admin/src/typings/app.d.ts
vendored
4
apps/admin/src/typings/app.d.ts
vendored
@ -633,11 +633,13 @@ declare namespace App {
|
|||||||
/** The backend service response data */
|
/** The backend service response data */
|
||||||
interface Response<T = unknown> {
|
interface Response<T = unknown> {
|
||||||
/** The backend service response code */
|
/** The backend service response code */
|
||||||
code: string
|
code: number
|
||||||
/** The backend service response message */
|
/** The backend service response message */
|
||||||
msg: string
|
msg: string
|
||||||
/** The backend service response data */
|
/** The backend service response data */
|
||||||
data: T
|
data: T
|
||||||
|
/** The backend service response success status */
|
||||||
|
success?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The demo backend service response data */
|
/** The demo backend service response data */
|
||||||
|
|||||||
@ -1,23 +1,26 @@
|
|||||||
|
/* eslint-disable ts/no-unsafe-function-type */
|
||||||
export class EventBus {
|
export class EventBus {
|
||||||
private listeners: Record<string, Function[]> = {};
|
private listeners: Record<string, Function[]> = {}
|
||||||
|
|
||||||
on(event: string, callback: Function) {
|
on(event: string, callback: Function) {
|
||||||
if (!this.listeners[event]) {
|
if (!this.listeners[event]) {
|
||||||
this.listeners[event] = [];
|
this.listeners[event] = []
|
||||||
}
|
}
|
||||||
this.listeners[event].push(callback);
|
this.listeners[event].push(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
off(event: string, callback: Function) {
|
off(event: string, callback: Function) {
|
||||||
if (!this.listeners[event]) return;
|
if (!this.listeners[event])
|
||||||
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
|
return
|
||||||
|
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(event: string, data?: any) {
|
emit(event: string, data?: any) {
|
||||||
if (!this.listeners[event]) return;
|
if (!this.listeners[event])
|
||||||
this.listeners[event].forEach(cb => cb(data));
|
return
|
||||||
|
this.listeners[event].forEach(cb => cb(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const $mitt = new EventBus();
|
export const $mitt = new EventBus()
|
||||||
export const open_book_topic_edit = 'open_book_topic_edit';
|
export const open_book_topic_edit = 'open_book_topic_edit'
|
||||||
|
|||||||
184
apps/admin/src/views/template/template-detail/README.md
Normal file
184
apps/admin/src/views/template/template-detail/README.md
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
# 答题卡排版编辑器 (Template Editor) 业务流程文档
|
||||||
|
|
||||||
|
本文档详细描述了 `src/views/template/template-detail` 模块的业务流程与核心逻辑。
|
||||||
|
|
||||||
|
## 1. 核心功能概述
|
||||||
|
|
||||||
|
该模块主要用于创建和编辑答题卡模板。用户可以上传底图(支持图片或 PDF),在底图上通过配置参数批量生成答题区域,并对生成的区域进行拖拽、缩放、旋转等排版操作,最终保存模板数据到后端。
|
||||||
|
|
||||||
|
## 2. 目录结构
|
||||||
|
|
||||||
|
- `index.vue`: **主入口文件**。负责整体状态管理、组件组装(Canvas + Settings)、数据初始化、保存逻辑以及核心的“生成区域”逻辑。
|
||||||
|
- `hooks/useUpload.ts`: **上传逻辑封装**。负责文件处理(PDF 转图片)、OSS 上传、Token 获取等。
|
||||||
|
- `modules/TemplateCanvas.vue`: **画布组件**。负责渲染底图、绘制答题区域、处理区域的交互(拖拽、缩放、旋转、多选/单选、对齐辅助线)。
|
||||||
|
- `modules/TemplateSettings.vue`: **设置面板组件**。负责展示模板基础信息、提供区域生成的参数配置(行列、宽高、间距)、展示已生成的区域列表。
|
||||||
|
|
||||||
|
## 3. 详细业务流程
|
||||||
|
|
||||||
|
### 3.1 初始化 (Initialization)
|
||||||
|
|
||||||
|
1. **加载页面**: 进入 `index.vue`。
|
||||||
|
2. **获取详情**: `onMounted` 钩子调用 `initData`。
|
||||||
|
- 检查 URL 参数 `id`。
|
||||||
|
- 如果有 `id`,调用 `fetchTemplateDetail(id)` 获取模板详情。
|
||||||
|
- **数据回填**:
|
||||||
|
- 将 `Name`, `Width`, `Height` 回填到 `templateInfo`。
|
||||||
|
- 将 `BackGroundUrl` 回填到 `imgSrc` 和 `templateInfo.backGroundUrl`。
|
||||||
|
- 解析 `TempContent` (JSON 字符串) 回填到 `regions` 列表。
|
||||||
|
|
||||||
|
### 3.2 上传底图 (Upload Background)
|
||||||
|
|
||||||
|
逻辑主要在 `hooks/useUpload.ts` 中实现:
|
||||||
|
|
||||||
|
1. **触发上传**: 用户点击“上传底稿”。
|
||||||
|
2. **上传检查 (`handleUploadCheck`)**:
|
||||||
|
- 如果已有底图,弹出确认框,提示“更换底图将清空当前已生成的布局”。
|
||||||
|
- 确认后打开文件选择器。
|
||||||
|
3. **文件处理 (`handleUpload`)**:
|
||||||
|
- **PDF 处理**: 使用 `pdfjs-dist` 渲染 PDF 第一页,按 A4 比例(2100px 宽)转换为 Canvas,再导出为 Blob。
|
||||||
|
- **图片处理**: 直接读取图片文件。
|
||||||
|
4. **OSS 上传**:
|
||||||
|
- 调用 `getAliOssTokenAxios` 获取 STS Token。
|
||||||
|
- 使用 `ali-oss` 客户端将文件上传至 OSS。
|
||||||
|
5. **状态更新**:
|
||||||
|
- 更新 `imgSrc` 为 OSS 返回的 URL。
|
||||||
|
- 更新 `templateInfo` 的宽高。
|
||||||
|
- **清空现有区域** (`regions`) 和选中状态 (`selectedRegionIds`)。
|
||||||
|
|
||||||
|
### 3.3 区域生成 (Region Generation)
|
||||||
|
|
||||||
|
逻辑在 `index.vue` 的 `handleGenerate` 中:
|
||||||
|
|
||||||
|
1. **配置参数**: 用户在 `TemplateSettings` 面板输入行列数、宽高、间距。
|
||||||
|
2. **计算编号**:
|
||||||
|
- 遍历现有 `regions`,解析 `label` (如 "Q1", "Q2"),找到最大编号 `maxNum`。
|
||||||
|
- 新生成的区域编号从 `maxNum + 1` 开始递增。
|
||||||
|
3. **批量创建**:
|
||||||
|
- 根据行 (`rows`)、列 (`cols`) 双重循环。
|
||||||
|
- 计算每个区域的 `x, y` 坐标:`start + index * (size + gap)`。
|
||||||
|
- 为这批区域生成同一个 `groupId` (UUID)。
|
||||||
|
4. **自动选中**:
|
||||||
|
- 将新生成的区域加入 `regions` 列表。
|
||||||
|
- 如果生成了多个区域,自动将它们全部加入 `selectedRegionIds`,方便用户立即进行批量拖拽。
|
||||||
|
|
||||||
|
### 3.4 画布交互 (Canvas Interaction)
|
||||||
|
|
||||||
|
逻辑在 `modules/TemplateCanvas.vue` 中,核心依赖 `vue3-draggable-resizable`:
|
||||||
|
|
||||||
|
1. **渲染**:
|
||||||
|
- 底图作为背景。
|
||||||
|
- 遍历 `regions` 渲染 `VueDraggableResizable` 组件。
|
||||||
|
2. **选择逻辑**:
|
||||||
|
- **单选**: 点击某个区域,清除其他选中,只选中当前。
|
||||||
|
- **多选**: 按住 Shift/Ctrl/Meta 点击,切换当前区域的选中状态。
|
||||||
|
- **取消选中**: 点击画布空白处 (`onCanvasMouseDown`),清空 `selectedRegionIds`。
|
||||||
|
- **从多选切换单选**: 在多选状态下,**松开鼠标** (`onRegionMouseUp`) 且没有发生拖拽(位移 < 3px)时,切换为单选当前区域。
|
||||||
|
3. **拖拽与移动 (`onDragging`)**:
|
||||||
|
- **单体移动**: 拖拽单个元素。
|
||||||
|
- **批量移动**: 如果当前拖拽的元素在选中集合中,计算其位移差 (`deltaX`, `deltaY`),同步更新所有选中元素的坐标。
|
||||||
|
- **辅助线**: 计算当前拖拽元素中心点与画布中心点的距离,小于阈值时显示吸附辅助线。
|
||||||
|
4. **缩放与旋转**:
|
||||||
|
- **缩放**: 通过组件自带句柄调整 `w, h`。
|
||||||
|
- **旋转**: 自定义旋转手柄,计算鼠标角度变化,更新 `rotation` 属性。
|
||||||
|
|
||||||
|
### 3.5 保存 (Save)
|
||||||
|
|
||||||
|
逻辑在 `index.vue` 的 `handleSave` 中:
|
||||||
|
|
||||||
|
1. **数据组装**:
|
||||||
|
- 收集 `templateInfo` (名称、宽高、背景图)。
|
||||||
|
- 将 `regions` 数组序列化为 JSON 字符串 (`tempContent`)。
|
||||||
|
2. **API 调用**: 调用 `fetchAddTemplate` 提交数据。
|
||||||
|
3. **反馈**: 成功后提示并返回上一页。
|
||||||
|
|
||||||
|
## 4. 数据结构说明
|
||||||
|
|
||||||
|
### Region (区域)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Region {
|
||||||
|
id: string // 唯一标识
|
||||||
|
groupId: string // 批次ID (同一次生成的区域共享)
|
||||||
|
x: number // X坐标
|
||||||
|
y: number // Y坐标
|
||||||
|
w: number // 宽度
|
||||||
|
h: number // 高度
|
||||||
|
label: string // 标签 (如 Q1)
|
||||||
|
selected: boolean // (前端临时状态)
|
||||||
|
rotation?: number // 旋转角度
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TemplateInfo (模板信息)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TemplateInfo {
|
||||||
|
name: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
backGroundUrl?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心原理
|
||||||
|
|
||||||
|
我们在前端渲染时,是基于 A4 标准尺寸 (210mm × 297mm) 进行比例换算的。
|
||||||
|
|
||||||
|
- 校验逻辑 :如果系统认为当前画布宽度对应 210mm,那么我们可以在画布上绘制一个 10mm × 10mm 的标准网格。
|
||||||
|
- 测试方法 :只需要上传一张 带有刻度尺 或 已知网格 的标准 A4 图片/PDF,开启系统网格,观察系统的红线是否与图片上的刻度重合,即可验证位置和比例是否精确。
|
||||||
|
|
||||||
|
### 网格辅助层和开关控制,具体修改如下:
|
||||||
|
|
||||||
|
1. TemplateCanvas.vue (画布组件)
|
||||||
|
- 新增了 showGrid 属性和网格渲染层。
|
||||||
|
- 自动计算:根据当前画布宽度( templateInfo.width )与 A4 宽度(210mm)的比例,动态计算出 10mm 在屏幕上的像素值。
|
||||||
|
- 使用 SVG pattern 技术绘制高精度的 10mm 标准红线网格。
|
||||||
|
|
||||||
|
2. index.vue (主页面)
|
||||||
|
- 在顶部工具栏增加了 "显示网格/隐藏网格" 按钮。
|
||||||
|
- 现在的操作栏顺序为:返回 -> 标题 -> 网格开关 -> 上传按钮 -> 保存按钮。
|
||||||
|
|
||||||
|
### 如何进行测试(操作步骤)
|
||||||
|
|
||||||
|
1. 准备测试底图 :找UI设计一张 A4 尺寸 (210x297mm) 的图片或 PDF,在上面画上 10mm 间隔的标尺或网格。(系统已支持直接上传 PDF,会自动渲染为高清底图)。
|
||||||
|
2. 上传底图 :在系统中点击“上传底稿”上传该文件。
|
||||||
|
3. 开启网格 :点击顶部新增的 "显示网格" 按钮。
|
||||||
|
4. 视觉验证 :
|
||||||
|
- 观察系统生成的 红色虚线网格 是否与您图片上的 标尺/网格 完美重合。
|
||||||
|
- 如果重合,说明系统的坐标映射与物理 A4 纸张是完全对应的。
|
||||||
|
- 如果不重合(例如系统网格偏大或偏小),则说明上传的图片本身比例不是标准的 A4 比例,或者存在边缘留白问题。
|
||||||
|
|
||||||
|
### A4 尺寸与像素换算说明
|
||||||
|
|
||||||
|
下面是页面渲染在页面的真实图片的信息:
|
||||||
|
Rendered size: 2100 × 2970 px
|
||||||
|
Rendered aspect ratio: 70∶99
|
||||||
|
Intrinsic size: 2100 × 2969 px
|
||||||
|
Intrinsic aspect ratio: 2100∶2969
|
||||||
|
File size: 526 kB
|
||||||
|
Current source: https://oss.qyzhjy.com/temp/1769655687535/book.jpg
|
||||||
|
|
||||||
|
比如生成出来的图片尺寸 2100px × 2970px , ,这是系统专门针对 A4 纸张优化的标准分辨率。
|
||||||
|
|
||||||
|
1. 换算公式
|
||||||
|
我们的代码(在 useUpload.ts 中)在处理 PDF 转图片时,强制将宽度设定为 2100px 。基于 A4 标准宽度 210mm ,换算关系如下:
|
||||||
|
[ o bj ec tO bj ec t ] 1 mm = 10 px
|
||||||
|
这个比例非常便于计算:
|
||||||
|
|
||||||
|
- 宽度 : [ o bj ec tO bj ec t ] 210 mm × 10 = 2100 px
|
||||||
|
- 高度 : [ o bj ec tO bj ec t ] 297 mm × 10 = 2970 px
|
||||||
|
- DPI : [ o bj ec tO bj ec t ] 10 px/mm × 25.4 ≈ 254 DPI 2. 如何验证比例是否正确?
|
||||||
|
是的,只要确保**比例(Aspect Ratio)**是对的,且系统能正确识别这是 A4 纸即可。
|
||||||
|
|
||||||
|
我在代码中已经为您实现了 动态网格系统 ,它不依赖固定的像素值,而是根据当前图片宽度反向计算:
|
||||||
|
|
||||||
|
1. 系统读取当前图片宽度(例如 2100px)。
|
||||||
|
2. 系统知道这是 A4 纸(210mm)。
|
||||||
|
3. 系统自动计算出 10mm = (2100 / 21) = 100px 。
|
||||||
|
4. 系统在屏幕上绘制间隔为 100px 的网格线。
|
||||||
|
验证方法: 如果您上传了一张分辨率较低的图片(例如 1050px 宽),系统会自动计算出 1mm = 5px ,网格线会变为 50px 间隔。 只要网格线能与您图片上的物理刻度对齐,就说明换算和位置是绝对精准的。
|
||||||
|
5. 结论
|
||||||
|
|
||||||
|
- 您当前的图片 2100 x 2970 是 完美标准 。
|
||||||
|
- 如果您上传其他尺寸,只要长宽比接近 A4 (1:1.414),我们的网格系统都能自动适配并准确显示 10mm 刻度。
|
||||||
|
- 您可以使用我在顶部新增的 “显示网格” 按钮来直观验证这一点。
|
||||||
@ -3,10 +3,10 @@
|
|||||||
import type { GenSettings, Region, TemplateInfo } from './types'
|
import type { GenSettings, Region, TemplateInfo } from './types'
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { NButton, NUpload, useMessage } from 'naive-ui'
|
import { NButton, NUpload, useMessage } from 'naive-ui'
|
||||||
import { ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useRouterPush } from '@/hooks/common/router'
|
import { useRouterPush } from '@/hooks/common/router'
|
||||||
import { fetchAddTemplate } from '@/service/api/template'
|
import { fetchAddTemplate, fetchTemplateDetail } from '@/service/api/template'
|
||||||
import { useUpload } from './hooks/useUpload'
|
import { useUpload } from './hooks/useUpload'
|
||||||
import TemplateCanvas from './modules/TemplateCanvas.vue'
|
import TemplateCanvas from './modules/TemplateCanvas.vue'
|
||||||
import TemplateSettings from './modules/TemplateSettings.vue'
|
import TemplateSettings from './modules/TemplateSettings.vue'
|
||||||
@ -17,16 +17,16 @@ const { routerBack } = useRouterPush()
|
|||||||
|
|
||||||
// 模版基础信息
|
// 模版基础信息
|
||||||
const templateInfo = ref<TemplateInfo>({
|
const templateInfo = ref<TemplateInfo>({
|
||||||
name: '期末大考标准答题卡',
|
name: '',
|
||||||
width: 2100,
|
width: 2100,
|
||||||
height: 2994,
|
height: 2997,
|
||||||
})
|
})
|
||||||
|
|
||||||
const imgSrc = ref('')
|
const imgSrc = ref('')
|
||||||
|
|
||||||
// 生成配置
|
// 生成配置
|
||||||
const genSettings = ref<GenSettings>({
|
const genSettings = ref<GenSettings>({
|
||||||
cols: 1,
|
cols: 6,
|
||||||
rows: 1,
|
rows: 1,
|
||||||
w: 296,
|
w: 296,
|
||||||
h: 309,
|
h: 309,
|
||||||
@ -42,6 +42,11 @@ const regions = ref<Region[]>([])
|
|||||||
// 选中区域集合
|
// 选中区域集合
|
||||||
const selectedRegionIds = ref<Set<string>>(new Set())
|
const selectedRegionIds = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
|
// 视图控制
|
||||||
|
const showGrid = ref(false)// 是否显示网格
|
||||||
|
const showRuler = ref(true)// 是否显示标尺
|
||||||
|
const showCoordinates = ref(false)// 是否显示坐标
|
||||||
|
|
||||||
// Use Hook
|
// Use Hook
|
||||||
const { loading, handleUpload, handleUploadCheck } = useUpload(imgSrc, templateInfo, regions, selectedRegionIds)
|
const { loading, handleUpload, handleUploadCheck } = useUpload(imgSrc, templateInfo, regions, selectedRegionIds)
|
||||||
|
|
||||||
@ -135,6 +140,12 @@ async function handleSave() {
|
|||||||
}
|
}
|
||||||
console.log(params, 'params')
|
console.log(params, 'params')
|
||||||
|
|
||||||
|
// 校验
|
||||||
|
if (!params.name) {
|
||||||
|
message.error('请输入模板名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const { error } = await fetchAddTemplate(params)
|
const { error } = await fetchAddTemplate(params)
|
||||||
|
|
||||||
if (!error) {
|
if (!error) {
|
||||||
@ -142,6 +153,54 @@ async function handleSave() {
|
|||||||
routerBack()
|
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 } = data || {}
|
||||||
|
templateInfo.value = {
|
||||||
|
name: Name,
|
||||||
|
width: Width,
|
||||||
|
height: Height,
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -157,12 +216,43 @@ async function handleSave() {
|
|||||||
<span class="text-lg font-medium">答题卡排版编辑器 V2.0</span>
|
<span class="text-lg font-medium">答题卡排版编辑器 V2.0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<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
|
<NUpload
|
||||||
ref="uploadRef"
|
ref="uploadRef" :show-file-list="false" :custom-request="handleUpload"
|
||||||
:show-file-list="false"
|
accept=".pdf,image/png,image/jpeg,image/jpg" style="display: none"
|
||||||
:custom-request="handleUpload"
|
|
||||||
accept=".pdf,image/png,image/jpeg,image/jpg"
|
|
||||||
style="display: none"
|
|
||||||
/>
|
/>
|
||||||
<NButton @click="handleUploadTrigger">
|
<NButton @click="handleUploadTrigger">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@ -181,12 +271,13 @@ async function handleSave() {
|
|||||||
<TemplateCanvas
|
<TemplateCanvas
|
||||||
v-model:template-info="templateInfo" v-model:regions="regions"
|
v-model:template-info="templateInfo" v-model:regions="regions"
|
||||||
v-model:selected-region-ids="selectedRegionIds" :img-src="imgSrc" :loading="loading"
|
v-model:selected-region-ids="selectedRegionIds" :img-src="imgSrc" :loading="loading"
|
||||||
|
:show-grid="showGrid" :show-ruler="showRuler" :show-coordinates="showCoordinates"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Right: Settings Panel -->
|
<!-- Right: Settings Panel -->
|
||||||
<TemplateSettings
|
<TemplateSettings
|
||||||
v-model:template-info="templateInfo" v-model:gen-settings="genSettings"
|
v-model:template-info="templateInfo" v-model:gen-settings="genSettings" :regions="regions"
|
||||||
:regions="regions" @generate="handleGenerate" @delete-region="deleteRegion" @clear-all="clearAll"
|
@generate="handleGenerate" @delete-region="deleteRegion" @clear-all="clearAll"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import type { Region, TemplateInfo } from '../types'
|
import type { Region, TemplateInfo } from '../types'
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { NButton, NScrollbar, NSpin } from 'naive-ui'
|
import { NButton, NScrollbar, NSpin } from 'naive-ui'
|
||||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import VueDraggableResizable from 'vue3-draggable-resizable'
|
import VueDraggableResizable from 'vue3-draggable-resizable'
|
||||||
import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css'
|
import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css'
|
||||||
|
|
||||||
@ -12,6 +12,9 @@ const props = defineProps<{
|
|||||||
selectedRegionIds: Set<string>
|
selectedRegionIds: Set<string>
|
||||||
imgSrc: string
|
imgSrc: string
|
||||||
loading: boolean
|
loading: boolean
|
||||||
|
showGrid?: boolean
|
||||||
|
showRuler?: boolean
|
||||||
|
showCoordinates?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -46,6 +49,95 @@ watch(() => props.imgSrc, () => {
|
|||||||
const dragStartPositions = ref<Map<string, { x: number, y: number }>>(new Map())
|
const dragStartPositions = ref<Map<string, { x: number, y: number }>>(new Map())
|
||||||
const mouseDownPos = ref<{ x: number, y: number } | null>(null)
|
const mouseDownPos = ref<{ x: number, y: number } | null>(null)
|
||||||
|
|
||||||
|
// --- 鼠标坐标显示 ---
|
||||||
|
const mouseCoord = ref({ x: 0, y: 0 })
|
||||||
|
const showMouseCoord = ref(false)
|
||||||
|
|
||||||
|
function onCanvasMouseMove(e: MouseEvent) {
|
||||||
|
if (!props.showCoordinates) {
|
||||||
|
showMouseCoord.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取画布元素
|
||||||
|
const canvasEl = e.currentTarget as HTMLElement
|
||||||
|
if (!canvasEl)
|
||||||
|
return
|
||||||
|
|
||||||
|
const rect = canvasEl.getBoundingClientRect()
|
||||||
|
|
||||||
|
// 计算相对于画布左上角的像素坐标 (考虑缩放 scale)
|
||||||
|
// 注意:e.clientX/Y 是视口坐标,rect.left/top 也是视口坐标
|
||||||
|
// 鼠标在 canvas 元素内的像素位置
|
||||||
|
const x = (e.clientX - rect.left) / scale.value
|
||||||
|
const y = (e.clientY - rect.top) / scale.value
|
||||||
|
|
||||||
|
// 转换为毫米 (1mm = 10px)
|
||||||
|
mouseCoord.value = {
|
||||||
|
x: Math.round(x / 10),
|
||||||
|
y: Math.round(y / 10),
|
||||||
|
}
|
||||||
|
showMouseCoord.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCanvasMouseLeave() {
|
||||||
|
showMouseCoord.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 网格系统 ---
|
||||||
|
// A4 纸张宽度 210mm
|
||||||
|
const A4_WIDTH_MM = 210
|
||||||
|
|
||||||
|
// 计算 10mm 在当前画布宽度下的像素值
|
||||||
|
const gridSizePx = computed(() => {
|
||||||
|
if (!props.templateInfo.width)
|
||||||
|
return 0
|
||||||
|
const pixelsPerMm = props.templateInfo.width / A4_WIDTH_MM
|
||||||
|
return pixelsPerMm * 10 // 10mm
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- 标尺生成逻辑 ---
|
||||||
|
const rulerPathH = computed(() => {
|
||||||
|
if (!gridSizePx.value || !props.templateInfo.width)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
let d = ''
|
||||||
|
// 10mm 一格
|
||||||
|
const step = gridSizePx.value
|
||||||
|
const count = Math.ceil(props.templateInfo.width / step)
|
||||||
|
|
||||||
|
for (let i = 0; i <= count; i++) {
|
||||||
|
const x = i * step
|
||||||
|
// 大刻度 (10mm)
|
||||||
|
d += `M${x} 0 V20 `
|
||||||
|
// 中间加 1 个 5mm 刻度
|
||||||
|
const half = x + step / 2
|
||||||
|
if (half < props.templateInfo.width) {
|
||||||
|
d += `M${half} 12 V20 `
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
})
|
||||||
|
|
||||||
|
const rulerPathV = computed(() => {
|
||||||
|
if (!gridSizePx.value || !props.templateInfo.height)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
let d = ''
|
||||||
|
const step = gridSizePx.value
|
||||||
|
const count = Math.ceil(props.templateInfo.height / step)
|
||||||
|
|
||||||
|
for (let i = 0; i <= count; i++) {
|
||||||
|
const y = i * step
|
||||||
|
d += `M0 ${y} H20 `
|
||||||
|
const half = y + step / 2
|
||||||
|
if (half < props.templateInfo.height) {
|
||||||
|
d += `M12 ${half} H20 `
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
})
|
||||||
|
|
||||||
// --- 拖拽与调整大小逻辑 ---
|
// --- 拖拽与调整大小逻辑 ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -316,14 +408,93 @@ onUnmounted(() => {
|
|||||||
width: `${templateInfo.width}px`,
|
width: `${templateInfo.width}px`,
|
||||||
height: `${templateInfo.height}px`,
|
height: `${templateInfo.height}px`,
|
||||||
}"
|
}"
|
||||||
@mousedown="onCanvasMouseDown"
|
|
||||||
>
|
>
|
||||||
|
<!-- 标尺 (外部) -->
|
||||||
|
<template v-if="showRuler && gridSizePx > 0">
|
||||||
|
<!-- 横向标尺 -->
|
||||||
|
<div class="absolute left-0 h-[20px] w-full border-b border-gray-300 bg-white/80 backdrop-blur-sm -top-[20px]">
|
||||||
|
<svg width="100%" height="20" class="overflow-visible">
|
||||||
|
<path :d="rulerPathH" stroke="#666" stroke-width="1" fill="none" />
|
||||||
|
<!-- 刻度值 -->
|
||||||
|
<text
|
||||||
|
v-for="i in Math.ceil(templateInfo.width / gridSizePx)"
|
||||||
|
:key="`h-${i}`"
|
||||||
|
:x="i * gridSizePx + 2"
|
||||||
|
y="10"
|
||||||
|
font-size="22"
|
||||||
|
fill="#666"
|
||||||
|
>{{ i * 10 }}</text>
|
||||||
|
<text x="2" y="10" font-size="10" fill="#666">0</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<!-- 纵向标尺 -->
|
||||||
|
<div class="absolute top-0 h-full w-[20px] border-r border-gray-300 bg-white/80 backdrop-blur-sm -left-[20px]">
|
||||||
|
<svg width="20" height="100%" class="overflow-visible">
|
||||||
|
<path :d="rulerPathV" stroke="#666" stroke-width="1" fill="none" />
|
||||||
|
<!-- 刻度值 -->
|
||||||
|
<text
|
||||||
|
v-for="i in Math.ceil(templateInfo.height / gridSizePx)"
|
||||||
|
:key="`v-${i}`"
|
||||||
|
x="2"
|
||||||
|
:y="i * gridSizePx + 10"
|
||||||
|
font-size="22"
|
||||||
|
fill="#666"
|
||||||
|
transform-origin="lef"
|
||||||
|
style="writing-mode: vertical-lr;"
|
||||||
|
>{{ i * 10 }}</text>
|
||||||
|
<text x="2" y="10" font-size="10" fill="#666" style="writing-mode: vertical-lr;">0</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 实际画布交互层 -->
|
||||||
|
<div
|
||||||
|
class="relative h-full w-full"
|
||||||
|
@mousedown="onCanvasMouseDown"
|
||||||
|
@mousemove="onCanvasMouseMove"
|
||||||
|
@mouseleave="onCanvasMouseLeave"
|
||||||
|
>
|
||||||
|
<!-- 鼠标坐标提示 (跟随鼠标或固定角落) -->
|
||||||
|
<div
|
||||||
|
v-if="showMouseCoord"
|
||||||
|
class="pointer-events-none absolute left-0 top-0 z-50 rounded bg-black/75 px-2 py-1 text-80px text-white backdrop-blur-sm transition-opacity"
|
||||||
|
:style="{
|
||||||
|
transform: `translate(${mouseCoord.x * 10 + 15}px, ${mouseCoord.y * 10 + 15}px)`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
X: {{ mouseCoord.x }}mm, Y: {{ mouseCoord.y }}mm
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 背景图片 -->
|
<!-- 背景图片 -->
|
||||||
<img
|
<img
|
||||||
v-if="imgSrc" :src="imgSrc"
|
v-if="imgSrc" :src="imgSrc"
|
||||||
class="pointer-events-none absolute inset-0 h-full w-full select-none object-contain"
|
class="pointer-events-none absolute inset-0 h-full w-full select-none object-contain"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<!-- 网格辅助层 (10mm 格子) -->
|
||||||
|
<svg
|
||||||
|
v-if="showGrid && gridSizePx > 0"
|
||||||
|
class="pointer-events-none absolute inset-0 z-1 h-full w-full"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern
|
||||||
|
id="gridPattern"
|
||||||
|
:width="gridSizePx"
|
||||||
|
:height="gridSizePx"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
:d="`M ${gridSizePx} 0 L 0 0 0 ${gridSizePx}`"
|
||||||
|
fill="none"
|
||||||
|
stroke="rgba(255, 0, 0, 0.3)"
|
||||||
|
stroke-width="1"
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" fill="url(#gridPattern)" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
<!-- 对齐参考线 -->
|
<!-- 对齐参考线 -->
|
||||||
<div
|
<div
|
||||||
v-if="guides.x !== null"
|
v-if="guides.x !== null"
|
||||||
@ -398,6 +569,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</NScrollbar>
|
</NScrollbar>
|
||||||
|
|
||||||
<!-- 缩放控制 -->
|
<!-- 缩放控制 -->
|
||||||
|
|||||||
@ -99,7 +99,7 @@ const localGenSettings = computed({
|
|||||||
</NForm>
|
</NForm>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 bg-gray-50 p-4">
|
<div class="max-h-[520px] flex-1 overflow-auto bg-gray-50 p-4">
|
||||||
<div class="mb-2 flex items-center justify-between">
|
<div class="mb-2 flex items-center justify-between">
|
||||||
<span class="text-xs text-gray-500">已生成布局列表</span>
|
<span class="text-xs text-gray-500">已生成布局列表</span>
|
||||||
<NButton v-if="regions.length" text type="error" size="tiny" @click="$emit('clearAll')">
|
<NButton v-if="regions.length" text type="error" size="tiny" @click="$emit('clearAll')">
|
||||||
@ -116,7 +116,7 @@ const localGenSettings = computed({
|
|||||||
<div
|
<div
|
||||||
v-for="region in regions"
|
v-for="region in regions"
|
||||||
:key="region.id"
|
:key="region.id"
|
||||||
class="group-item relative border rounded bg-white p-3 shadow-sm transition hover:shadow-md"
|
class="group-item relative max-h-[120px] overflow-auto border rounded bg-white p-3 shadow-sm transition hover:shadow-md"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -100,19 +100,16 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
|
|||||||
width: 430,
|
width: 430,
|
||||||
render: row => (
|
render: row => (
|
||||||
<div class="flex-center gap-8px">
|
<div class="flex-center gap-8px">
|
||||||
<NButton type="primary" ghost size="small" onClick={() => edit(row.ID)}>
|
<NButton size="small" onClick={() => { }}>
|
||||||
基础信息
|
预览
|
||||||
|
</NButton>
|
||||||
|
<NButton size="small" type="primary" ghost onClick={() => handlePageInfo(row.ID)}>
|
||||||
|
页面信息
|
||||||
</NButton>
|
</NButton>
|
||||||
<NButton size="small" onClick={() => { }}>
|
<NButton size="small" onClick={() => { }}>
|
||||||
铺码
|
铺码
|
||||||
</NButton>
|
</NButton>
|
||||||
<NButton size="small" onClick={() => { }}>
|
<NButton size="small" onClick={() => { handlePrint(row) }}>
|
||||||
预览
|
|
||||||
</NButton>
|
|
||||||
<NButton size="small" onClick={() => handlePageInfo(row.ID)}>
|
|
||||||
页面信息
|
|
||||||
</NButton>
|
|
||||||
<NButton size="small" onClick={() => { }}>
|
|
||||||
打印
|
打印
|
||||||
</NButton>
|
</NButton>
|
||||||
<NPopconfirm onPositiveClick={() => handleDelete((row as any).ID || row.id)}>
|
<NPopconfirm onPositiveClick={() => handleDelete((row as any).ID || row.id)}>
|
||||||
@ -135,7 +132,6 @@ const {
|
|||||||
drawerVisible,
|
drawerVisible,
|
||||||
operateType,
|
operateType,
|
||||||
editingData,
|
editingData,
|
||||||
handleEdit,
|
|
||||||
checkedRowKeys,
|
checkedRowKeys,
|
||||||
onBatchDeleted,
|
onBatchDeleted,
|
||||||
onDeleted,
|
onDeleted,
|
||||||
@ -154,16 +150,21 @@ async function handleDelete(id: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit(id: number) {
|
|
||||||
handleEdit(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePageInfo(id: number) {
|
function handlePageInfo(id: number) {
|
||||||
routerPushByKey('template_template-detail', { query: { id: id.toString() } })
|
routerPushByKey('template_template-detail', { query: { id: id.toString() } })
|
||||||
}
|
}
|
||||||
function handleAdd() {
|
function handleAdd() {
|
||||||
routerPushByKey('template_template-detail')
|
routerPushByKey('template_template-detail')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handlePrint(row: Api.Template.CommonRecord) {
|
||||||
|
const { BackGroundUrl } = row
|
||||||
|
if (!BackGroundUrl) {
|
||||||
|
window.$message?.error('打印模板不存在')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.open(BackGroundUrl)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export default readStarConfig(
|
|||||||
'**/node_modules/**',
|
'**/node_modules/**',
|
||||||
'**/public/**',
|
'**/public/**',
|
||||||
'pnpm-lock.yaml',
|
'pnpm-lock.yaml',
|
||||||
|
'**/*.md',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user