调试修改爬虫
This commit is contained in:
383
schoolNewsWeb/backend-api-implementation.md
Normal file
383
schoolNewsWeb/backend-api-implementation.md
Normal file
@@ -0,0 +1,383 @@
|
||||
# 后端新增接口实现文档
|
||||
|
||||
## 接口说明
|
||||
更新数据采集项状态接口,用于在创建资源后更新采集项的状态和关联的资源ID。
|
||||
|
||||
---
|
||||
|
||||
## 1. Controller层
|
||||
|
||||
**文件路径**: `schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/controller/DataCollectionItemController.java`
|
||||
|
||||
```java
|
||||
package org.xyzh.crontab.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.xyzh.api.crontab.DataCollectionItemService;
|
||||
import org.xyzh.common.domain.ResultDomain;
|
||||
import org.xyzh.common.dto.crontab.TbDataCollectionItem;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 数据采集项Controller
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/crontab/collection/item")
|
||||
public class DataCollectionItemController {
|
||||
|
||||
@Autowired
|
||||
private DataCollectionItemService dataCollectionItemService;
|
||||
|
||||
/**
|
||||
* 更新数据采集项状态
|
||||
* PUT /api/crontab/collection/item/{itemId}/status
|
||||
*
|
||||
* @param itemId 采集项ID
|
||||
* @param request 更新请求体
|
||||
* @return 更新结果
|
||||
*/
|
||||
@PutMapping("/{itemId}/status")
|
||||
public ResultDomain<String> updateCollectionItemStatus(
|
||||
@PathVariable String itemId,
|
||||
@RequestBody UpdateStatusRequest request) {
|
||||
|
||||
try {
|
||||
// 参数校验
|
||||
if (itemId == null || itemId.trim().isEmpty()) {
|
||||
return ResultDomain.fail("采集项ID不能为空");
|
||||
}
|
||||
|
||||
if (request.getResourceId() == null || request.getResourceId().trim().isEmpty()) {
|
||||
return ResultDomain.fail("资源ID不能为空");
|
||||
}
|
||||
|
||||
// 查询采集项
|
||||
TbDataCollectionItem item = dataCollectionItemService.getById(itemId);
|
||||
if (item == null) {
|
||||
return ResultDomain.fail("采集项不存在");
|
||||
}
|
||||
|
||||
// 更新采集项状态
|
||||
item.setStatus(request.getStatus() != null ? request.getStatus() : 1); // 默认为已转换
|
||||
item.setResourceId(request.getResourceId());
|
||||
item.setProcessTime(new Date());
|
||||
|
||||
// 如果传入了处理人,也更新
|
||||
if (request.getProcessor() != null) {
|
||||
item.setProcessor(request.getProcessor());
|
||||
}
|
||||
|
||||
// 保存更新
|
||||
boolean success = dataCollectionItemService.updateById(item);
|
||||
|
||||
if (success) {
|
||||
return ResultDomain.success("更新成功", request.getResourceId());
|
||||
} else {
|
||||
return ResultDomain.fail("更新失败");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return ResultDomain.fail("更新失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态请求体
|
||||
*/
|
||||
public static class UpdateStatusRequest {
|
||||
/** 状态: 0-未处理 1-已转换 2-已忽略 */
|
||||
private Integer status;
|
||||
/** 资源ID */
|
||||
private String resourceId;
|
||||
/** 处理人 */
|
||||
private String processor;
|
||||
|
||||
public Integer getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Integer status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getResourceId() {
|
||||
return resourceId;
|
||||
}
|
||||
|
||||
public void setResourceId(String resourceId) {
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
|
||||
public String getProcessor() {
|
||||
return processor;
|
||||
}
|
||||
|
||||
public void setProcessor(String processor) {
|
||||
this.processor = processor;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Service层 (如果需要)
|
||||
|
||||
**文件路径**: `schoolNewsServ/api/api-crontab/src/main/java/org/xyzh/api/crontab/DataCollectionItemService.java`
|
||||
|
||||
如果Service接口中没有 `getById` 和 `updateById` 方法,需要添加:
|
||||
|
||||
```java
|
||||
package org.xyzh.api.crontab;
|
||||
|
||||
import org.xyzh.common.dto.crontab.TbDataCollectionItem;
|
||||
|
||||
/**
|
||||
* 数据采集项Service接口
|
||||
*/
|
||||
public interface DataCollectionItemService {
|
||||
|
||||
/**
|
||||
* 根据ID查询采集项
|
||||
* @param itemId 采集项ID
|
||||
* @return 采集项数据
|
||||
*/
|
||||
TbDataCollectionItem getById(String itemId);
|
||||
|
||||
/**
|
||||
* 更新采集项
|
||||
* @param item 采集项数据
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateById(TbDataCollectionItem item);
|
||||
|
||||
// ... 其他方法
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Service实现层
|
||||
|
||||
**文件路径**: `schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/service/impl/DataCollectionItemServiceImpl.java`
|
||||
|
||||
```java
|
||||
package org.xyzh.crontab.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.xyzh.api.crontab.DataCollectionItemService;
|
||||
import org.xyzh.common.dto.crontab.TbDataCollectionItem;
|
||||
import org.xyzh.crontab.mapper.DataCollectionItemMapper;
|
||||
|
||||
/**
|
||||
* 数据采集项Service实现
|
||||
*/
|
||||
@Service
|
||||
public class DataCollectionItemServiceImpl implements DataCollectionItemService {
|
||||
|
||||
@Autowired
|
||||
private DataCollectionItemMapper dataCollectionItemMapper;
|
||||
|
||||
@Override
|
||||
public TbDataCollectionItem getById(String itemId) {
|
||||
QueryWrapper<TbDataCollectionItem> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("id", itemId);
|
||||
return dataCollectionItemMapper.selectOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateById(TbDataCollectionItem item) {
|
||||
int rows = dataCollectionItemMapper.updateById(item);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
// ... 其他方法实现
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据库表结构确认
|
||||
|
||||
确保 `tb_data_collection_item` 表有以下字段:
|
||||
|
||||
```sql
|
||||
-- 如果没有这些字段,需要添加
|
||||
ALTER TABLE tb_data_collection_item
|
||||
ADD COLUMN IF NOT EXISTS resource_id VARCHAR(50) COMMENT '转换后的资源ID';
|
||||
|
||||
ALTER TABLE tb_data_collection_item
|
||||
ADD COLUMN IF NOT EXISTS process_time DATETIME COMMENT '处理时间';
|
||||
|
||||
ALTER TABLE tb_data_collection_item
|
||||
ADD COLUMN IF NOT EXISTS processor VARCHAR(50) COMMENT '处理人';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 接口测试
|
||||
|
||||
### 请求示例
|
||||
|
||||
**请求URL**: `PUT http://localhost:8080/api/crontab/collection/item/{itemId}/status`
|
||||
|
||||
**请求Headers**:
|
||||
```
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求Body**:
|
||||
```json
|
||||
{
|
||||
"status": 1,
|
||||
"resourceId": "resource_123456",
|
||||
"processor": "admin"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
**成功响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "更新成功",
|
||||
"data": "resource_123456",
|
||||
"code": 200
|
||||
}
|
||||
```
|
||||
|
||||
**失败响应**:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "采集项不存在",
|
||||
"code": 500
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 前端调用示例
|
||||
|
||||
前端已经在 `src/apis/crontab/index.ts` 中添加了调用方法:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 更新采集项状态为已转换
|
||||
* @param itemId 采集项ID
|
||||
* @param resourceId 资源ID
|
||||
* @returns Promise<ResultDomain<string>>
|
||||
*/
|
||||
async updateCollectionItemStatus(itemId: string, resourceId: string): Promise<ResultDomain<string>> {
|
||||
const response = await api.put<string>(`${this.baseUrl}/collection/item/${itemId}/status`, {
|
||||
status: 1, // 已转换
|
||||
resourceId
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
```
|
||||
|
||||
使用方式:
|
||||
```typescript
|
||||
await crontabApi.updateCollectionItemStatus('item_123', 'resource_456');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 完整流程
|
||||
|
||||
```
|
||||
1. 用户在 ArticleAdd 组件中编辑文章
|
||||
↓
|
||||
2. 点击"立即发布"
|
||||
↓
|
||||
3. 前端调用: POST /api/news/resources/resource
|
||||
创建资源,返回 resourceID
|
||||
↓
|
||||
4. 前端调用: PUT /api/crontab/collection/item/{itemId}/status
|
||||
传入参数:
|
||||
{
|
||||
"status": 1,
|
||||
"resourceId": "新创建的resourceID"
|
||||
}
|
||||
↓
|
||||
5. 后端更新 tb_data_collection_item:
|
||||
- status = 1 (已转换)
|
||||
- resource_id = resourceID
|
||||
- process_time = NOW()
|
||||
↓
|
||||
6. 前端刷新列表,采集项状态显示"已转换"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 注意事项
|
||||
|
||||
1. **权限控制**: 建议添加权限校验,只有管理员可以调用此接口
|
||||
2. **事务处理**: 如果涉及多表操作,建议添加 `@Transactional` 注解
|
||||
3. **日志记录**: 建议添加操作日志,记录谁在什么时间转换了哪个采集项
|
||||
4. **并发控制**: 如果存在并发转换的情况,建议添加乐观锁
|
||||
5. **参数校验**: 可以使用 `@Valid` 和 `@NotBlank` 等注解进行参数校验
|
||||
|
||||
---
|
||||
|
||||
## 9. 可选:添加操作日志
|
||||
|
||||
```java
|
||||
/**
|
||||
* 更新数据采集项状态
|
||||
*/
|
||||
@PutMapping("/{itemId}/status")
|
||||
@Log(title = "更新采集项状态", businessType = BusinessType.UPDATE)
|
||||
public ResultDomain<String> updateCollectionItemStatus(
|
||||
@PathVariable String itemId,
|
||||
@RequestBody UpdateStatusRequest request) {
|
||||
// ... 实现代码
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Mapper层 (如果使用MyBatis Plus)
|
||||
|
||||
**文件路径**: `schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/mapper/DataCollectionItemMapper.java`
|
||||
|
||||
```java
|
||||
package org.xyzh.crontab.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.xyzh.common.dto.crontab.TbDataCollectionItem;
|
||||
|
||||
/**
|
||||
* 数据采集项Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface DataCollectionItemMapper extends BaseMapper<TbDataCollectionItem> {
|
||||
// MyBatis Plus 已经提供了 selectById, updateById 等方法
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完成清单
|
||||
|
||||
- [ ] 复制 Controller 代码到 `DataCollectionItemController.java`
|
||||
- [ ] 检查 Service 接口是否有 `getById` 和 `updateById` 方法
|
||||
- [ ] 如果没有,添加 Service 实现
|
||||
- [ ] 确认数据库表有 `resource_id`, `process_time`, `processor` 字段
|
||||
- [ ] 如果没有,执行 SQL 添加字段
|
||||
- [ ] 重启后端服务
|
||||
- [ ] 使用 Postman 或前端测试接口
|
||||
- [ ] 验证数据库数据是否正确更新
|
||||
|
||||
---
|
||||
|
||||
完成以上步骤后,前端的转换功能就可以正常工作了!
|
||||
@@ -243,6 +243,17 @@ export const crontabApi = {
|
||||
tagId
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新采集项状态为已转换
|
||||
* @param itemId 采集项ID
|
||||
* @param status 状态
|
||||
* @returns Promise<ResultDomain<string>>
|
||||
*/
|
||||
async updateCollectionItemStatus(itemId: string, status: number): Promise<ResultDomain<string>> {
|
||||
const response = await api.put<string>(`${this.baseUrl}/collection/item/${itemId}/status/${status}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -140,6 +140,9 @@ export interface DataCollectionItem extends BaseDTO {
|
||||
processTime?: string;
|
||||
/** 处理人 */
|
||||
processor?: string;
|
||||
/** 解析状态 */
|
||||
itemExecuteStatus?: number;
|
||||
itemExecuteMessage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -135,13 +135,24 @@
|
||||
|
||||
<!-- 爬虫解析结果 -->
|
||||
<el-table-column label="解析结果" width="220">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="getStatusTagType(row.itemExecuteStatus)"
|
||||
size="small"
|
||||
>
|
||||
{{ getAnalyzeStatus(row.itemExecuteStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 来源 -->
|
||||
<el-table-column label="来源" width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="parse-result">
|
||||
<div v-if="row.category" class="result-item">
|
||||
<el-tag size="small" type="info">{{ row.category }}</el-tag>
|
||||
</div>
|
||||
<div v-if="row.source" class="result-item">
|
||||
来源: {{ row.source }}
|
||||
{{ row.source }}
|
||||
</div>
|
||||
<div v-if="row.tags" class="result-item">
|
||||
标签: {{ row.tags }}
|
||||
@@ -341,6 +352,7 @@
|
||||
<ArticleAdd
|
||||
v-if="convertDialogVisible"
|
||||
:initial-data="convertFormData"
|
||||
:collection-item-id="convertItem?.id"
|
||||
:show-back-button="false"
|
||||
@publish-success="handleConvertSuccess"
|
||||
@back="convertDialogVisible = false"
|
||||
@@ -501,7 +513,8 @@ function handleViewDetail(row: DataCollectionItem) {
|
||||
// ==================== 转换操作 ====================
|
||||
|
||||
/**
|
||||
* 处理富文本内容,清理不必要的样式
|
||||
* 处理富文本内容,清理可能导致冲突的样式
|
||||
* 采用温和策略:只移除明显有问题的样式,保留大部分原始格式
|
||||
*/
|
||||
function cleanHtmlContent(html: string): string {
|
||||
if (!html) return '';
|
||||
@@ -510,32 +523,61 @@ function cleanHtmlContent(html: string): string {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
|
||||
// 移除所有内联样式中的字体大小、字体族等可能导致显示问题的样式
|
||||
// 需要移除的问题样式属性(这些通常会导致显示问题)
|
||||
const problematicStyles = [
|
||||
'font-family', // 字体族可能不存在
|
||||
'font-size', // 字体大小可能过大或过小
|
||||
'line-height', // 行高可能不适配
|
||||
'width', // 固定宽度可能导致布局问题
|
||||
'height', // 固定高度可能导致内容截断
|
||||
'max-width', // 最大宽度限制
|
||||
'max-height', // 最大高度限制
|
||||
'position', // 定位可能导致布局混乱
|
||||
'top', 'left', 'right', 'bottom', // 定位相关
|
||||
'z-index', // 层级可能冲突
|
||||
'float', // 浮动可能导致布局问题
|
||||
];
|
||||
|
||||
// 处理所有带有内联样式的元素
|
||||
const elementsWithStyle = tempDiv.querySelectorAll('[style]');
|
||||
elementsWithStyle.forEach((el) => {
|
||||
const element = el as HTMLElement;
|
||||
const style = element.style;
|
||||
|
||||
// 保留一些重要的样式,移除可能冲突的样式
|
||||
// 收集所有当前样式
|
||||
const preservedStyles: string[] = [];
|
||||
for (let i = 0; i < style.length; i++) {
|
||||
const property = style[i];
|
||||
const value = style.getPropertyValue(property);
|
||||
|
||||
// 保留文本颜色
|
||||
if (style.color) preservedStyles.push(`color: ${style.color}`);
|
||||
// 保留背景色
|
||||
if (style.backgroundColor) preservedStyles.push(`background-color: ${style.backgroundColor}`);
|
||||
// 保留文本对齐
|
||||
if (style.textAlign) preservedStyles.push(`text-align: ${style.textAlign}`);
|
||||
// 保留边距
|
||||
if (style.marginTop) preservedStyles.push(`margin-top: ${style.marginTop}`);
|
||||
if (style.marginBottom) preservedStyles.push(`margin-bottom: ${style.marginBottom}`);
|
||||
// 如果不在问题样式列表中,则保留
|
||||
if (!problematicStyles.includes(property) && value) {
|
||||
preservedStyles.push(`${property}: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
element.setAttribute('style', preservedStyles.join('; '));
|
||||
// 重新设置样式
|
||||
if (preservedStyles.length > 0) {
|
||||
element.setAttribute('style', preservedStyles.join('; '));
|
||||
} else {
|
||||
element.removeAttribute('style');
|
||||
}
|
||||
});
|
||||
|
||||
// 移除可能的外部类名,避免样式冲突
|
||||
// 只移除明显的外部框架类名(如bootstrap、tailwind等)
|
||||
const problematicClassPrefixes = ['col-', 'row-', 'container', 'flex-', 'grid-', 'd-', 'p-', 'm-', 'w-', 'h-'];
|
||||
const elementsWithClass = tempDiv.querySelectorAll('[class]');
|
||||
elementsWithClass.forEach((el) => {
|
||||
el.removeAttribute('class');
|
||||
const classList = el.className.split(' ').filter(cls => {
|
||||
// 如果类名以问题前缀开头,则移除
|
||||
return !problematicClassPrefixes.some(prefix => cls.startsWith(prefix));
|
||||
});
|
||||
|
||||
if (classList.length > 0) {
|
||||
el.className = classList.join(' ');
|
||||
} else {
|
||||
el.removeAttribute('class');
|
||||
}
|
||||
});
|
||||
|
||||
return tempDiv.innerHTML;
|
||||
@@ -632,6 +674,14 @@ function getStatusText(status: number | undefined): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getAnalyzeStatus(executeStatus: number | undefined): string {
|
||||
switch (executeStatus) {
|
||||
case 0: return '解析失败';
|
||||
case 1: return '解析成功';
|
||||
default: return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态标签类型
|
||||
*/
|
||||
|
||||
@@ -115,6 +115,7 @@ import { RichTextComponent } from '@/components/text';
|
||||
import { FileUpload } from '@/components/file';
|
||||
import { ArticleShow } from '.';
|
||||
import { resourceTagApi, resourceApi } from '@/apis/resource';
|
||||
import { crontabApi } from '@/apis/crontab';
|
||||
import { ResourceVO, Tag, TagType } from '@/types/resource';
|
||||
|
||||
defineOptions({
|
||||
@@ -126,6 +127,7 @@ interface Props {
|
||||
showBackButton?: boolean;
|
||||
backButtonText?: string;
|
||||
initialData?: ResourceVO;
|
||||
collectionItemId?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -221,30 +223,61 @@ async function handlePublish() {
|
||||
await formRef.value?.validate();
|
||||
|
||||
publishing.value = true;
|
||||
if (isEdit.value) {
|
||||
const result = await resourceApi.updateResource(articleForm.value);
|
||||
if (result.success) {
|
||||
ElMessage.success('保存成功');
|
||||
emit('publish-success', result.data?.resource?.resourceID || '');
|
||||
} else {
|
||||
ElMessage.error(result.message || '保存失败');
|
||||
}
|
||||
|
||||
// 如果是从数据采集转换过来的,使用转换接口
|
||||
if (props.collectionItemId) {
|
||||
await handleConvertFromCollection();
|
||||
} else {
|
||||
// 普通创建资源
|
||||
const result = await resourceApi.createResource(articleForm.value);
|
||||
if (result.success) {
|
||||
const resourceID = result.data?.resource?.resourceID || '';
|
||||
ElMessage.success('发布成功');
|
||||
emit('publish-success', result.data?.resource?.resourceID || '');
|
||||
emit('publish-success', resourceID);
|
||||
} else {
|
||||
ElMessage.error(result.message || '发布失败');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发布失败:', error);
|
||||
ElMessage.error('发布失败');
|
||||
} finally {
|
||||
publishing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 从数据采集转换为资源
|
||||
async function handleConvertFromCollection() {
|
||||
if (!props.collectionItemId) return;
|
||||
|
||||
try {
|
||||
// 1. 先创建资源(使用用户编辑后的内容)
|
||||
const createResult = await resourceApi.createResource(articleForm.value);
|
||||
|
||||
if (!createResult.success) {
|
||||
ElMessage.error(createResult.message || '创建资源失败');
|
||||
return;
|
||||
}
|
||||
|
||||
const resourceID = createResult.data?.resource?.resourceID || '';
|
||||
|
||||
// 2. 更新采集项状态,关联resourceID
|
||||
try {
|
||||
await crontabApi.updateCollectionItemStatus(props.collectionItemId, 1);
|
||||
console.log('采集项状态已更新, resourceID:', resourceID);
|
||||
} catch (error) {
|
||||
console.warn('更新采集项状态失败:', error);
|
||||
// 不影响主流程,资源已经创建成功
|
||||
}
|
||||
|
||||
ElMessage.success('转换成功');
|
||||
emit('publish-success', resourceID);
|
||||
} catch (error) {
|
||||
console.error('转换失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存草稿
|
||||
async function handleSaveDraft() {
|
||||
savingDraft.value = true;
|
||||
|
||||
Reference in New Issue
Block a user