Commit 37792b38 by pangchong

feat(api): 完善API请求适配及添加OpenAPI规范文档

- 添加VITE_PARTITION_NAME环境变量,支持动态配置分区名称
- 在vite.config.ts中新增本地开发代理配置,适配后端接口路径
- 优化api/index.ts中请求处理逻辑,支持FormData的Content-Type自动处理
- 新增postJson和deleteJson方法以支持JSON格式的POST和DELETE请求
- 统一接口返回码判断,增加code和success字段缺失时设为200的兼容处理
- 调整接口基础路径获取逻辑,区分管理端和普通接口
- 导出serviceManage用于管理端接口调用
- 制定并完善全局组件使用规范,规范表单及标签组件使用
- 完善XML编辑器节点操作偏好设置联动逻辑,支持自动展开节点插入行为
- 新增完整的OpenAPI JSON规范文件,覆盖翻译、XML处理、系统状态等多个接口
- OpenAPI文档包含翻译功能、批量处理、文件下载、标签管理等丰富的业务能力描述
parent 48fdf105
VITE_PARTITION_NAME = 'from_project_001'
\ No newline at end of file
......@@ -189,12 +189,15 @@
---
## 16. 全局选择框与输入框占位符规范
- **统一使用 `CommonSelect`**:在所有业务界面中,**禁止**直接使用 Naive UI 原生的 `n-select`**强制要求**统一使用项目封装好的全局组件 `CommonSelect`
## 16. 全局常用表单、选择与状态组件规范 (强制)
- **统一使用全局组件**:在所有业务界面中,对于选择框、多选框、数字输入框与状态标签,**禁止**直接使用 Naive UI 原生的组件,**强制要求**统一使用项目封装好的全局组件进行替代:
- **下拉选择框**:使用 **`CommonSelect`** 替代 `n-select`。对 `CommonSelect` 监听值变更回调时,**必须**绑定组件导出的 **`@change`** 事件,**禁止**使用 `@update:value`
- **数字输入框**:使用 **`CommonInputNumber`** 替代 `n-input-number`。若绑定业务数据需要强制保留 number 类型,应显式配置属性 **`:strict="true"`**
- **多选/单选框组**:使用 **`CommonCheckbox`** 替代 `n-checkbox-group` 及原生的 `v-for` 循环列表。对于需要使用单个 `n-checkbox` 作为独立布尔值开关的场景,可通过将绑定的响应式变量类型转为 `string[]`,并对接传入 `:options="[{ label: '文案', value: 'yes' }]"` 来完全对齐 `<CommonCheckbox>` 全局组件的使用。
- **状态标签**:使用 **`CommonTag`** 替代 `n-tag`。支持插槽、字典配置及 autoColor 自适应着色。
- **禁用冗余的 placeholder 占位符**
- 在编写 `CommonSelect``n-input` 组件时,对于普通属性、枚举选择或内容输入,**禁止**手动添加 `placeholder="请选择"``placeholder="选择"``placeholder="请输入"` 等冗余的占位字符。
- 只在具有明确指示性特指文案的业务场景下(如“请选择切换动画”等特有场景),方可添加具体的占位文本。
- **事件监听规范**:对 `CommonSelect` 组件监听值变更回调时,**必须**绑定组件导出的 **`@change`** 事件(如 `@change="onTagChange"`),**禁止**使用 `@update:value` 监听器,以保障数据联动更新的生命周期一致性。
---
......@@ -202,3 +205,12 @@
- **禁止直接在业务代码或常量中硬编码 XML 节点标签数组或映射**(如 `['PARA', 'PARAC', 'TITLE']` 等元素集合)。
- **强制要求统一收拢管理**:所有关于 DTD 定义的各类 XML 标签、节点分类(如文档型标签、列表条目标签、复杂容器标签、头部元数据标签等)必须统一在 [xmlTags.ts](file:///e:/refactor-Editor/Ifar-Xml-Editor/src/configs/xmlTags.ts) 文件中进行定义和导出。
- **使用规范**:在任何业务组件、逻辑 Hook 或常量模块中需要判断或使用节点标签集合时,必须先在 `xmlTags.ts` 中维护,随后在对应的代码文件中通过 `import` 引用,严禁私自在局部硬编码私有数组,以确保 DTD 分类规则的一致性与后期维护的便捷度。
---
## 18. XML 编辑器节点操作与偏好设置联动规范
- **偏好设置与行为控制**:节点操作(如新增节点、粘贴 XML 片段等)和表格操作(如新增行、新增列等)应当尊重用户的全局偏好设置。
- **插入节点自动展开 (autoExpandOnInsert) 规范**
- 如果开启了“插入节点自动展开”偏好设置,在树上新增/粘贴节点或在表格新增行列时,应当自动展开新增节点自身(以及包含的子节点结构),并展开对应的父节点。
- 如果关闭了该设置,在新增/粘贴节点或新增表格行列时,**必须跳过**对新增节点、其子节点以及其所有父/祖先节点的展开操作,保持原有的折叠状态。
- **实现要求**:为防止选中节点更新(`selectedNodeId`)时自动触发的树祖先节点级联展开动作,必须在选中变化前设置 store 的 `skipExpandOnSelect` 状态进行拦截限制,随后在 `nextTick` 中恢复,以实现无级联展开的静默插入。
......@@ -50,7 +50,14 @@ const createService = (baseURL: string) => {
if (isJson) {
json = (await response.json()) as ResponseData
if (json) {
if (json.code === 200 || json.code === '200' || json.code === 0 || json.code === '0' || json.success === true) {
if (
json.code === 200 ||
json.code === '200' ||
json.code === 0 ||
json.code === '0' ||
json.success === true ||
(json.code === undefined && json.success === undefined)
) {
json.code = 200
return json
}
......@@ -76,7 +83,14 @@ const createService = (baseURL: string) => {
}
if (json) {
if (json.code === 200 || json.code === '200' || json.code === 0 || json.code === '0' || json.success === true) {
if (
json.code === 200 ||
json.code === '200' ||
json.code === 0 ||
json.code === '0' ||
json.success === true ||
(json.code === undefined && json.success === undefined)
) {
json.code = 200
}
}
......@@ -176,7 +190,11 @@ const createService = (baseURL: string) => {
const isFormData = data instanceof FormData
const headers = { ...rest.headers }
if (!isFormData) {
if (isFormData) {
// FormData 不需要手动设置 Content-Type,浏览器会自动处理并加上 boundary
delete headers['Content-Type']
delete headers['content-type']
} else {
headers['Content-Type'] = 'application/x-www-form-urlencoded'
}
......@@ -203,13 +221,47 @@ const createService = (baseURL: string) => {
})
.send(true)
},
postJson<T = any>(url: string, data?: any, config?: RequestConfig) {
const { showLoading = false, msgField, ...rest } = config || {}
const headers = {
'Content-Type': 'application/json',
...rest.headers
}
return alovaInst
.Post<ResponseData<T>>(url, JSON.stringify(data), {
...rest,
headers,
meta: { ...rest.meta, showLoading, msgField }
})
.send(true)
},
put<T = any>(url: string, data?: any, config?: RequestConfig) {
const { showLoading = false, msgField, ...rest } = config || {}
return alovaInst.Put<ResponseData<T>>(url, data, { ...rest, meta: { ...rest.meta, showLoading, msgField } }).send(true)
const isFormData = data instanceof FormData
const headers = { ...rest.headers }
if (isFormData) {
delete headers['Content-Type']
delete headers['content-type']
}
return alovaInst.Put<ResponseData<T>>(url, data, { ...rest, headers, meta: { ...rest.meta, showLoading, msgField } }).send(true)
},
delete<T = any>(url: string, config?: RequestConfig) {
const { showLoading = false, msgField, ...rest } = config || {}
return alovaInst.Delete<ResponseData<T>>(url, { ...rest, meta: { ...rest.meta, showLoading, msgField } }).send(true)
return alovaInst.Delete<ResponseData<T>>(url, undefined, { ...rest, meta: { ...rest.meta, showLoading, msgField } }).send(true)
},
deleteJson<T = any>(url: string, data?: any, config?: RequestConfig) {
const { showLoading = false, msgField, ...rest } = config || {}
const headers = {
'Content-Type': 'application/json',
...rest.headers
}
return alovaInst
.Delete<ResponseData<T>>(url, JSON.stringify(data), {
...rest,
headers,
meta: { ...rest.meta, showLoading, msgField }
})
.send(true)
},
/** 下载文件 (支持流式下载及自动两步验证模式) */
async download(url: string, data?: any, fileName?: string, config?: RequestConfig) {
......@@ -229,7 +281,10 @@ const createService = (baseURL: string) => {
const isFormData = data instanceof FormData
const headers = { ...rest.headers }
if (!isFormData) {
if (isFormData) {
delete headers['Content-Type']
delete headers['content-type']
} else {
headers['Content-Type'] = 'application/x-www-form-urlencoded'
}
......@@ -339,15 +394,27 @@ const createService = (baseURL: string) => {
const getBaseURL = () => {
// 开发环境使用相对路径以便配合 vite.config.ts 的代理服务
if (import.meta.env.DEV) {
return '/api'
return '/translations'
}
// 生产/打包环境下,若配置了 VITE_APP_PROXY_URL,则将接口基准地址指向对应地址的 /api 路径
// 生产/打包环境下,若配置了 VITE_APP_PROXY_URL,则将接口基准地址指向对应地址的 /translations 路径
const proxyUrl = import.meta.env.VITE_APP_PROXY_URL
if (proxyUrl) {
const cleanUrl = proxyUrl.trim().replace(/\/$/, '')
return cleanUrl.endsWith('/api') ? cleanUrl : `${cleanUrl}/api`
return cleanUrl.endsWith('/translations') ? cleanUrl : `${cleanUrl}/translations`
}
return '/translations'
}
const getManageBaseURL = () => {
if (import.meta.env.DEV) {
return '/translationsManage'
}
const proxyUrl = import.meta.env.VITE_APP_MANAGE_PROXY_URL
if (proxyUrl) {
const cleanUrl = proxyUrl.trim().replace(/\/$/, '')
return cleanUrl.endsWith('/translationsManage') ? cleanUrl : `${cleanUrl}/translationsManage`
}
return '/api'
return '/translationsManage'
}
// 当前主要的域名服务
......@@ -355,6 +422,7 @@ export const service = createService(getBaseURL())
// 后续支持多个域名的示例(需要调用另一域名的接口时,直接导出并使用即可)
export const serviceDomain2 = createService('/apiDomain2')
export const serviceManage = createService(getManageBaseURL())
// 兼容老代码中直接引用 alovaInstance 的方式
export const alovaInstance = service.alova
{
"openapi": "3.1.0",
"info": { "title": "航空翻译API", "description": "提供航空领域专业翻译服务,支持向量搜索和LLM翻译", "version": "1.0.0" },
"paths": {
"/translate": {
"post": {
"tags": ["Translation"],
"summary": "翻译文本",
"description": "将航空技术文本翻译,支持英译中和中译英双向翻译,优先使用向量搜索结果,无匹配时使用LLM生成。可选择将LLM生成的翻译结果保存到数据库。",
"operationId": "translate_text_translate_post",
"parameters": [
{
"name": "fuzzy_score_threshold",
"in": "query",
"required": false,
"schema": { "type": "number", "description": "相似度阈值", "default": 90, "title": "Fuzzy Score Threshold" },
"description": "相似度阈值"
},
{
"name": "model_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "LLM模型名称", "default": "glm-4-flash", "title": "Model Name" },
"description": "LLM模型名称"
},
{
"name": "temperature",
"in": "query",
"required": false,
"schema": { "type": "number", "description": "LLM温度参数", "default": 0.3, "title": "Temperature" },
"description": "LLM温度参数"
}
],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/TranslationRequest" } } }
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/TranslationResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/translate/xml": {
"post": {
"tags": ["XML Translation"],
"summary": "翻译XML文档(支持多文件)",
"description": "上传一个或多个XML文档并翻译,返回下载链接。支持嵌套标签翻译功能,能够完整翻译包含子标签的XML元素内容。可以自定义翻译标签后缀和存储到Milvus数据库的分区名称。支持选择批量处理或并行处理方法,以及英译中或中译英方向。",
"operationId": "translate_xml_file_translate_xml_post",
"requestBody": {
"content": { "multipart/form-data": { "schema": { "$ref": "#/components/schemas/Body_translate_xml_file_translate_xml_post" } } },
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/XMLTranslationResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/translation/status/{file_id}": {
"get": {
"tags": ["XML Translation"],
"summary": "获取XML翻译状态(单个任务)",
"description": "获取单个XML翻译任务的当前状态",
"operationId": "get_translation_status_translation_status__file_id__get",
"parameters": [
{
"name": "file_id",
"in": "path",
"required": true,
"schema": { "type": "string", "description": "翻译任务的唯一标识符", "title": "File Id" },
"description": "翻译任务的唯一标识符"
}
],
"responses": {
"200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } },
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/download/xml/{file_id}": {
"get": {
"tags": ["XML Translation"],
"summary": "下载翻译后的XML文档",
"description": "通过文件ID下载翻译后的XML文档",
"operationId": "download_translated_xml_download_xml__file_id__get",
"parameters": [
{
"name": "file_id",
"in": "path",
"required": true,
"schema": { "type": "string", "description": "翻译任务的唯一标识符", "title": "File Id" },
"description": "翻译任务的唯一标识符"
}
],
"responses": {
"200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } },
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/translation/status": {
"get": {
"tags": ["XML Translation"],
"summary": "获取XML翻译状态(批量查询)",
"description": "获取多个XML翻译任务的当前状态,通过查询参数传入多个file_id,用逗号分隔或多次传入",
"operationId": "get_translation_status_batch_translation_status_get",
"parameters": [
{
"name": "file_ids",
"in": "query",
"required": true,
"schema": {
"type": "string",
"description": "翻译任务的唯一标识符列表,多个ID用逗号分隔,例如: id1,id2,id3",
"title": "File Ids"
},
"description": "翻译任务的唯一标识符列表,多个ID用逗号分隔,例如: id1,id2,id3"
}
],
"responses": {
"200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } },
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/xml/tags": {
"get": {
"tags": ["XML Translation"],
"summary": "获取当前目标标签",
"description": "获取用于XML翻译的目标标签列表",
"operationId": "get_xml_tags_xml_tags_get",
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/XMLTagsResponse" } } }
}
}
}
},
"/xml/tags/add": {
"post": {
"tags": ["XML Translation"],
"summary": "添加XML翻译目标标签",
"description": "添加用于XML翻译的目标标签",
"operationId": "add_xml_tags_xml_tags_add_post",
"requestBody": {
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/XMLTagsRequest" } } },
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/XMLTagsResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/xml/tags/remove": {
"post": {
"tags": ["XML Translation"],
"summary": "移除XML翻译目标标签",
"description": "移除用于XML翻译的目标标签",
"operationId": "remove_xml_tags_xml_tags_remove_post",
"requestBody": {
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/XMLTagsRequest" } } },
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/XMLTagsResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/xml/extract/analyze": {
"post": {
"tags": ["XML Extraction"],
"summary": "分析XML标签并推荐标签对",
"description": "上传XML文件,分析其中的标签并推荐可能的标签对",
"operationId": "analyze_xml_tags_xml_extract_analyze_post",
"requestBody": {
"content": {
"multipart/form-data": { "schema": { "$ref": "#/components/schemas/Body_analyze_xml_tags_xml_extract_analyze_post" } }
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/AnalyzeXMLTagsResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/xml/extract/preview": {
"post": {
"tags": ["XML Extraction"],
"summary": "预览XML翻译数据提取结果",
"description": "上传XML文件并预览提取的翻译对",
"operationId": "preview_xml_extraction_xml_extract_preview_post",
"requestBody": {
"content": {
"multipart/form-data": { "schema": { "$ref": "#/components/schemas/Body_preview_xml_extraction_xml_extract_preview_post" } }
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExtractXMLResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/xml/extract/process": {
"post": {
"tags": ["XML Extraction"],
"summary": "处理XML文件并提取翻译数据",
"description": "上传XML文件,提取翻译数据,可选择保存到Excel和导入到数据库",
"operationId": "process_xml_extraction_xml_extract_process_post",
"requestBody": {
"content": {
"multipart/form-data": { "schema": { "$ref": "#/components/schemas/Body_process_xml_extraction_xml_extract_process_post" } }
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExtractXMLResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/xml/extract/status/{file_id}": {
"get": {
"tags": ["XML Extraction"],
"summary": "获取XML提取状态",
"description": "获取XML提取任务的当前状态",
"operationId": "get_extraction_status_xml_extract_status__file_id__get",
"parameters": [
{
"name": "file_id",
"in": "path",
"required": true,
"schema": { "type": "string", "description": "提取任务的唯一标识符", "title": "File Id" },
"description": "提取任务的唯一标识符"
}
],
"responses": {
"200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } },
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/xml/extract/download/{file_id}/{filename}": {
"get": {
"tags": ["XML Extraction"],
"summary": "下载提取的Excel文件",
"description": "通过文件ID和文件名下载提取的Excel文件",
"operationId": "download_extraction_excel_xml_extract_download__file_id___filename__get",
"parameters": [
{
"name": "file_id",
"in": "path",
"required": true,
"schema": { "type": "string", "description": "提取任务的唯一标识符", "title": "File Id" },
"description": "提取任务的唯一标识符"
},
{
"name": "filename",
"in": "path",
"required": true,
"schema": { "type": "string", "description": "要下载的Excel文件名", "title": "Filename" },
"description": "要下载的Excel文件名"
}
],
"responses": {
"200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } },
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/xml/extract/batch": {
"post": {
"tags": ["XML Extraction"],
"summary": "批量处理XML文件并提取翻译数据",
"description": "上传多个XML文件,批量提取翻译数据,可选择保存到Excel和导入到数据库",
"operationId": "process_batch_xml_extraction_xml_extract_batch_post",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": { "$ref": "#/components/schemas/Body_process_batch_xml_extraction_xml_extract_batch_post" }
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExtractXMLResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/xml/extract/batch/status/{batch_id}": {
"get": {
"tags": ["XML Extraction"],
"summary": "获取批量XML提取状态",
"description": "获取批量XML提取任务的当前状态",
"operationId": "get_batch_extraction_status_xml_extract_batch_status__batch_id__get",
"parameters": [
{
"name": "batch_id",
"in": "path",
"required": true,
"schema": { "type": "string", "description": "批量提取任务的唯一标识符", "title": "Batch Id" },
"description": "批量提取任务的唯一标识符"
}
],
"responses": {
"200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } },
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/xml/extract/batch/download/{batch_id}/{filename}": {
"get": {
"tags": ["XML Extraction"],
"summary": "下载批量提取的Excel文件",
"description": "通过批次ID和文件名下载批量提取的Excel文件",
"operationId": "download_batch_extraction_excel_xml_extract_batch_download__batch_id___filename__get",
"parameters": [
{
"name": "batch_id",
"in": "path",
"required": true,
"schema": { "type": "string", "description": "批量提取任务的唯一标识符", "title": "Batch Id" },
"description": "批量提取任务的唯一标识符"
},
{
"name": "filename",
"in": "path",
"required": true,
"schema": { "type": "string", "description": "要下载的Excel文件名", "title": "Filename" },
"description": "要下载的Excel文件名"
}
],
"responses": {
"200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } },
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/milvus/partitions": {
"get": {
"tags": ["System"],
"summary": "获取Milvus集合的分区列表",
"description": "查询指定集合中的所有分区,包括分区名称和统计信息",
"operationId": "get_collection_partitions_milvus_partitions_get",
"parameters": [
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": {
"type": "string",
"description": "Milvus集合名称",
"default": "aviation_translations",
"title": "Collection Name"
},
"description": "Milvus集合名称"
}
],
"responses": {
"200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } },
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/health": {
"get": {
"tags": ["System"],
"summary": "健康检查",
"description": "检查API服务是否正常运行",
"operationId": "health_check_health_get",
"responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } } }
}
}
},
"components": {
"schemas": {
"AnalyzeXMLTagsResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"tag_counts": { "additionalProperties": { "type": "integer" }, "type": "object", "title": "Tag Counts", "default": {} },
"suggested_pairs": {
"items": { "prefixItems": [{ "type": "string" }, { "type": "string" }], "type": "array", "maxItems": 2, "minItems": 2 },
"type": "array",
"title": "Suggested Pairs",
"default": []
},
"error": { "type": "string", "title": "Error", "default": "" }
},
"type": "object",
"required": ["success"],
"title": "AnalyzeXMLTagsResponse"
},
"Body_analyze_xml_tags_xml_extract_analyze_post": {
"properties": { "file": { "type": "string", "format": "binary", "title": "File", "description": "要分析的XML文件" } },
"type": "object",
"required": ["file"],
"title": "Body_analyze_xml_tags_xml_extract_analyze_post"
},
"Body_preview_xml_extraction_xml_extract_preview_post": {
"properties": {
"file": { "type": "string", "format": "binary", "title": "File" },
"tag_pairs": {
"type": "string",
"title": "Tag Pairs",
"description": "JSON格式的标签对列表,例如: [{\"en_tag\":\"TITLE\",\"cn_tag\":\"TITLEC\"}]",
"default": "[{\"en_tag\":\"TITLE\",\"cn_tag\":\"TITLEC\"},{\"en_tag\":\"PARA\",\"cn_tag\":\"PARAC\"}]"
},
"exclude_empty": { "type": "boolean", "title": "Exclude Empty", "description": "是否排除空文本", "default": true },
"preview_limit": { "type": "integer", "title": "Preview Limit", "description": "预览数据的最大条目数", "default": 10 },
"enable_nested_extraction": {
"type": "boolean",
"title": "Enable Nested Extraction",
"description": "是否启用内嵌标签提取(默认True)",
"default": true
},
"skip_tags": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Skip Tags",
"description": "要跳过提取的XML标签(逗号分隔)。默认值已展示,可直接增删修改",
"default": "TOOLNBR,CONNBR,REFINT,REFBLOCK,REFEXT,EIN"
},
"enable_code_regex_skip": {
"type": "boolean",
"title": "Enable Code Regex Skip",
"description": "是否启用代码样式文本跳过(默认False)",
"default": false
}
},
"type": "object",
"required": ["file"],
"title": "Body_preview_xml_extraction_xml_extract_preview_post"
},
"Body_process_batch_xml_extraction_xml_extract_batch_post": {
"properties": {
"files": { "items": { "type": "string", "format": "binary" }, "type": "array", "title": "Files" },
"tag_pairs": {
"type": "string",
"title": "Tag Pairs",
"description": "JSON格式的标签对列表,例如: [{\"en_tag\":\"TITLE\",\"cn_tag\":\"TITLEC\"}]",
"default": "[{\"en_tag\":\"TITLE\",\"cn_tag\":\"TITLEC\"},{\"en_tag\":\"PARA\",\"cn_tag\":\"PARAC\"}]"
},
"exclude_empty": { "type": "boolean", "title": "Exclude Empty", "description": "是否排除空文本", "default": true },
"save_excel": { "type": "boolean", "title": "Save Excel", "description": "是否保存到Excel", "default": true },
"import_to_db": { "type": "boolean", "title": "Import To Db", "description": "是否导入到数据库", "default": false },
"partition_name": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Partition Name",
"description": "要导入的分区名称,如果为空则保存到默认位置"
},
"check_duplicates": { "type": "boolean", "title": "Check Duplicates", "description": "是否检查重复记录", "default": true },
"similarity_threshold": {
"type": "number",
"title": "Similarity Threshold",
"description": "相似度阈值,超过此值视为重复",
"default": 80
}
},
"type": "object",
"required": ["files"],
"title": "Body_process_batch_xml_extraction_xml_extract_batch_post"
},
"Body_process_xml_extraction_xml_extract_process_post": {
"properties": {
"file": { "type": "string", "format": "binary", "title": "File" },
"tag_pairs": {
"type": "string",
"title": "Tag Pairs",
"description": "JSON格式的标签对列表,例如: [{\"en_tag\":\"TITLE\",\"cn_tag\":\"TITLEC\"}]",
"default": "[{\"en_tag\":\"TITLE\",\"cn_tag\":\"TITLEC\"},{\"en_tag\":\"PARA\",\"cn_tag\":\"PARAC\"}]"
},
"exclude_empty": { "type": "boolean", "title": "Exclude Empty", "description": "是否排除空文本", "default": true },
"save_excel": { "type": "boolean", "title": "Save Excel", "description": "是否保存到Excel", "default": true },
"import_to_db": { "type": "boolean", "title": "Import To Db", "description": "是否导入到数据库", "default": false },
"partition_name": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Partition Name",
"description": "要导入的分区名称,如果为空则保存到默认位置"
},
"check_duplicates": { "type": "boolean", "title": "Check Duplicates", "description": "是否检查重复记录", "default": true },
"similarity_threshold": {
"type": "number",
"title": "Similarity Threshold",
"description": "相似度阈值,超过此值视为重复",
"default": 80
},
"enable_nested_extraction": {
"type": "boolean",
"title": "Enable Nested Extraction",
"description": "是否启用内嵌标签提取(默认True)",
"default": true
},
"skip_tags": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Skip Tags",
"description": "要跳过提取的XML标签(逗号分隔)。默认值已展示,可直接增删修改",
"default": "TOOLNBR,CONNBR,REFINT,REFBLOCK,REFEXT,EIN"
},
"enable_code_regex_skip": {
"type": "boolean",
"title": "Enable Code Regex Skip",
"description": "是否启用代码样式文本跳过(默认False)",
"default": false
}
},
"type": "object",
"required": ["file"],
"title": "Body_process_xml_extraction_xml_extract_process_post"
},
"Body_translate_xml_file_translate_xml_post": {
"properties": {
"files": { "items": { "type": "string", "format": "binary" }, "type": "array", "title": "Files" },
"max_workers": { "type": "integer", "title": "Max Workers", "description": "并行工作线程数", "default": 4 },
"batch_size": { "type": "integer", "title": "Batch Size", "description": "批量翻译的批次大小", "default": 10 },
"suffix": {
"type": "string",
"title": "Suffix",
"description": "翻译标签的后缀字母,默认为空(直接覆盖原标题);如需添加后缀可设置为'Z'等",
"default": ""
},
"database_partition_name": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Database Partition Name",
"description": "保存到Milvus数据库的分区名称(可选,默认为空,不传此参数则保存到默认位置)"
},
"model_name": {
"type": "string",
"title": "Model Name",
"description": "LLM模型名称:deepseek-chat,glm-4-flash,glm-4-plus,glm-4-air (默认: glm-4-flash)",
"default": "glm-4-flash"
},
"temperature": { "type": "number", "title": "Temperature", "description": "LLM温度参数", "default": 0.3 },
"collection_name": {
"type": "string",
"title": "Collection Name",
"description": "Milvus集合名称",
"default": "aviation_translations"
},
"query_partition_names": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Query Partition Names",
"description": "查询时使用的分区名称列表(可选,默认为空,不传此参数则不指定分区),多个分区用逗号分隔"
},
"target_tags": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Target Tags",
"description": "要翻译的目标XML标签(可选,默认为空,不传此参数则使用 /xml/tags 接口配置的默认标签),多个标签用逗号分隔"
},
"use_parallel_method": {
"type": "boolean",
"title": "Use Parallel Method",
"description": "是否使用并行处理方法(替代方案)",
"default": false
},
"search_direction": {
"type": "string",
"title": "Search Direction",
"description": "搜索方向,'en_to_zh'(英译中)或'zh_to_en'(中译英)",
"default": "en_to_zh"
},
"enable_nested_translation": {
"type": "boolean",
"title": "Enable Nested Translation",
"description": "是否启用嵌套标签翻译,能够翻译子标签内的文本内容",
"default": true
},
"skip_tags": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Skip Tags",
"description": "要跳过翻译的XML标签(逗号分隔)。默认值已展示,可直接增删修改",
"default": "TOOLNBR,CONNBR,REFINT,REFBLOCK,REFEXT,EIN"
},
"enable_code_regex_skip": {
"type": "boolean",
"title": "Enable Code Regex Skip",
"description": "是否启用基于正则的代码样式文本跳过(默认关闭)",
"default": false
}
},
"type": "object",
"required": ["files"],
"title": "Body_translate_xml_file_translate_xml_post"
},
"ExtractXMLResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"message": { "type": "string", "title": "Message" },
"total_count": { "type": "integer", "title": "Total Count", "default": 0 },
"preview_data": {
"items": { "$ref": "#/components/schemas/TranslationPair" },
"type": "array",
"title": "Preview Data",
"default": []
},
"excel_path": { "type": "string", "title": "Excel Path", "default": "" },
"import_count": { "type": "integer", "title": "Import Count", "default": 0 },
"file_id": { "type": "string", "title": "File Id", "default": "" },
"download_url": { "type": "string", "title": "Download Url", "default": "" },
"error": { "type": "string", "title": "Error", "default": "" }
},
"type": "object",
"required": ["success", "message"],
"title": "ExtractXMLResponse"
},
"HTTPValidationError": {
"properties": { "detail": { "items": { "$ref": "#/components/schemas/ValidationError" }, "type": "array", "title": "Detail" } },
"type": "object",
"title": "HTTPValidationError"
},
"TranslationPair": {
"properties": { "text": { "type": "string", "title": "Text" }, "translation": { "type": "string", "title": "Translation" } },
"type": "object",
"required": ["text", "translation"],
"title": "TranslationPair"
},
"TranslationRequest": {
"properties": {
"text": { "type": "string", "title": "Text" },
"partition_names": {
"anyOf": [{ "items": { "type": "string" }, "type": "array" }, { "type": "null" }],
"title": "Partition Names"
},
"stream": { "type": "boolean", "title": "Stream", "default": false },
"fuzzy_score_threshold": { "anyOf": [{ "type": "number" }, { "type": "null" }], "title": "Fuzzy Score Threshold", "default": 90 },
"search_direction": { "type": "string", "title": "Search Direction", "default": "en_to_zh" },
"save_to_database": { "type": "boolean", "title": "Save To Database", "default": false },
"database_partition_name": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Database Partition Name" }
},
"type": "object",
"required": ["text"],
"title": "TranslationRequest"
},
"TranslationResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"translation": { "type": "string", "title": "Translation" },
"is_direct_match": { "type": "boolean", "title": "Is Direct Match" },
"fuzzy_score": { "anyOf": [{ "type": "number" }, { "type": "null" }], "title": "Fuzzy Score" },
"reference_count": { "type": "integer", "title": "Reference Count" }
},
"type": "object",
"required": ["success", "translation", "is_direct_match", "reference_count"],
"title": "TranslationResponse"
},
"ValidationError": {
"properties": {
"loc": { "items": { "anyOf": [{ "type": "string" }, { "type": "integer" }] }, "type": "array", "title": "Location" },
"msg": { "type": "string", "title": "Message" },
"type": { "type": "string", "title": "Error Type" }
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError"
},
"XMLTagsRequest": {
"properties": { "tags": { "items": { "type": "string" }, "type": "array", "title": "Tags" } },
"type": "object",
"required": ["tags"],
"title": "XMLTagsRequest"
},
"XMLTagsResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"message": { "type": "string", "title": "Message" },
"tags": { "items": { "type": "string" }, "type": "array", "title": "Tags" }
},
"type": "object",
"required": ["success", "message", "tags"],
"title": "XMLTagsResponse"
},
"XMLTranslationResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"message": { "type": "string", "title": "Message" },
"file_id": { "type": "string", "title": "File Id" },
"download_url": { "type": "string", "title": "Download Url" },
"translated_tags_count": { "type": "integer", "title": "Translated Tags Count", "default": 0 },
"model_name": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Model Name" },
"temperature": { "anyOf": [{ "type": "number" }, { "type": "null" }], "title": "Temperature" },
"collection_name": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Collection Name" },
"query_partition_names": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Query Partition Names" },
"target_tags": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Target Tags" },
"use_parallel_method": { "anyOf": [{ "type": "boolean" }, { "type": "null" }], "title": "Use Parallel Method", "default": false },
"enable_nested_translation": {
"anyOf": [{ "type": "boolean" }, { "type": "null" }],
"title": "Enable Nested Translation",
"default": true
},
"skip_tags": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Skip Tags" },
"enable_code_regex_skip": {
"anyOf": [{ "type": "boolean" }, { "type": "null" }],
"title": "Enable Code Regex Skip",
"default": false
}
},
"type": "object",
"required": ["success", "message", "file_id", "download_url"],
"title": "XMLTranslationResponse"
}
}
},
"tags": [
{ "name": "Translation", "description": "翻译相关API" },
{ "name": "XML Translation", "description": "XML文档翻译API" },
{ "name": "XML Extraction", "description": "XML翻译数据提取API" },
{ "name": "System", "description": "系统相关API" }
]
}
{
"openapi": "3.1.0",
"info": { "title": "航空翻译数据管理API", "description": "用于管理Milvus中的航空翻译数据的API接口", "version": "1.0.0" },
"paths": {
"/collections/create": {
"post": {
"tags": ["Collection Management"],
"summary": "创建航空翻译集合",
"description": "创建一个新的航空翻译集合,包含BM25索引和必要的字段配置。",
"operationId": "create_collection_collections_create_post",
"parameters": [
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCollectionResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/collections/partitions": {
"post": {
"tags": ["Collection Management"],
"summary": "创建分区",
"description": "在航空翻译集合中创建一个新的分区。",
"operationId": "create_partition_collections_partitions_post",
"parameters": [
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreatePartitionRequest" } } }
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreatePartitionResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
},
"delete": {
"tags": ["Partition Management"],
"summary": "删除分区",
"description": "删除航空翻译集合中的指定分区。删除前会自动检查并释放分区。",
"operationId": "delete_partition_collections_partitions_delete",
"parameters": [
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeletePartitionRequest" } } }
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeletePartitionResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/data/import": {
"post": {
"tags": ["Data Import"],
"summary": "导入Excel数据",
"description": "从Excel文件导入航空翻译数据到Milvus集合。",
"operationId": "import_data_data_import_post",
"parameters": [
{
"name": "partition_name",
"in": "query",
"required": false,
"schema": { "anyOf": [{ "type": "string" }, { "type": "null" }], "description": "可选的分区名称", "title": "Partition Name" },
"description": "可选的分区名称"
},
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"requestBody": {
"required": true,
"content": { "multipart/form-data": { "schema": { "$ref": "#/components/schemas/Body_import_data_data_import_post" } } }
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImportDataResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/search": {
"post": {
"tags": ["Search"],
"summary": "搜索翻译数据",
"description": "在航空翻译数据中执行搜索,支持文本匹配和全文搜索两种模式,以及英中双向翻译。",
"operationId": "search_translations_search_post",
"parameters": [
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchRequest" } } }
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/health": {
"get": {
"tags": ["System"],
"summary": "Health Check",
"description": "服务健康检查\n\n返回:\n dict: 包含服务状态信息",
"operationId": "health_check_health_get",
"parameters": [
{
"name": "partition_name",
"in": "query",
"required": false,
"schema": { "anyOf": [{ "type": "string" }, { "type": "null" }], "description": "可选的分区名称", "title": "Partition Name" },
"description": "可选的分区名称"
},
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"responses": {
"200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } },
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/collections/load": {
"post": {
"tags": ["Collection Management"],
"summary": "加载航空翻译集合",
"description": "加载整个航空翻译集合到内存中。",
"operationId": "load_collection_collections_load_post",
"parameters": [
{
"name": "load_fields",
"in": "query",
"required": false,
"schema": {
"anyOf": [{ "type": "array", "items": { "type": "string" } }, { "type": "null" }],
"description": "可选的要加载的字段列表",
"title": "Load Fields"
},
"description": "可选的要加载的字段列表"
},
{
"name": "skip_load_dynamic_field",
"in": "query",
"required": false,
"schema": { "type": "boolean", "description": "是否跳过加载动态字段", "default": false, "title": "Skip Load Dynamic Field" },
"description": "是否跳过加载动态字段"
},
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCollectionResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/collections/release": {
"post": {
"tags": ["Collection Management"],
"summary": "释放航空翻译集合",
"description": "释放整个航空翻译集合从内存中。",
"operationId": "release_collection_collections_release_post",
"parameters": [
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCollectionResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/partitions/load": {
"post": {
"tags": ["Partition Management"],
"summary": "加载分区",
"description": "加载指定分区到内存中。",
"operationId": "load_partition_partitions_load_post",
"parameters": [
{
"name": "partition_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "要加载的分区名称", "default": "from_api", "title": "Partition Name" },
"description": "要加载的分区名称"
},
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoadPartitionResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/partitions/release": {
"post": {
"tags": ["Partition Management"],
"summary": "释放分区",
"description": "释放指定分区从内存中。",
"operationId": "release_partition_partitions_release_post",
"parameters": [
{
"name": "partition_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "要释放的分区名称", "default": "from_api", "title": "Partition Name" },
"description": "要释放的分区名称"
},
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoadPartitionResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/collections/delete": {
"delete": {
"tags": ["Collection Management"],
"summary": "删除航空翻译集合",
"description": "删除指定的航空翻译集合。",
"operationId": "delete_collection_collections_delete_delete",
"parameters": [
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCollectionResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/collections/load_state": {
"get": {
"tags": ["Collection Management"],
"summary": "获取集合加载状态",
"description": "获取航空翻译集合的加载状态。",
"operationId": "get_collection_load_state_collections_load_state_get",
"parameters": [
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"responses": {
"200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } },
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/collections/describe": {
"get": {
"tags": ["Collection Management"],
"summary": "获取集合详细描述",
"description": "获取指定集合的详细描述信息,包括字段、索引、加载状态等。",
"operationId": "describe_collection_collections_describe_get",
"parameters": [
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/DescribeCollectionResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/collections/alter_field": {
"post": {
"tags": ["Collection Management"],
"summary": "修改集合字段属性",
"description": "修改指定集合中现有字段的属性。\n\n支持修改的属性:\n- VARCHAR字段: max_length (最大字符长度)\n- ARRAY字段: max_capacity (数组最大容量)\n- 所有字段: mmap_enabled (内存映射启用状态)\n\n注意:不能添加新字段、删除字段或修改字段类型。",
"operationId": "alter_collection_field_collections_alter_field_post",
"parameters": [
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlterFieldRequest" } } }
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlterFieldResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/data/insert_single": {
"post": {
"tags": ["Data Import"],
"summary": "插入单条翻译数据",
"description": "插入单条翻译数据到指定分区。",
"operationId": "insert_single_translation_data_insert_single_post",
"parameters": [
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/InsertSingleRequest" } } }
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/InsertSingleResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/data/upsert": {
"post": {
"tags": ["Data Import"],
"summary": "更新或插入翻译数据",
"description": "更新或插入翻译数据。如果数据中的主键已存在,则更新该记录;如果不存在,则插入新记录。\n\n**请求示例:**\n\n单条数据更新(指定ID):\n```json\n{\n \"data\": {\n \"id\": 123,\n \"text\": \"aircraft engine\",\n \"translation\": \"航空发动机\"\n },\n \"partition_name\": \"aviation_engine\"\n}\n```\n\n单条数据插入(不指定ID,自动生成):\n```json\n{\n \"data\": {\n \"text\": \"flight control system\",\n \"translation\": \"飞行控制系统\"\n }\n}\n```\n\n单条数据插入(使用基于内容的确定性ID):\n```json\n{\n \"data\": {\n \"text\": \"flight control system\",\n \"translation\": \"飞行控制系统\"\n },\n \"use_content_based_id\": true\n}\n```\n\n批量数据操作(混合更新和插入):\n```json\n{\n \"data\": [\n {\n \"id\": 123,\n \"text\": \"updated aircraft engine\",\n \"translation\": \"更新的航空发动机\"\n },\n {\n \"text\": \"new navigation system\",\n \"translation\": \"新的导航系统\"\n }\n ],\n \"partition_name\": \"aviation_systems\"\n}\n```",
"operationId": "upsert_translations_data_upsert_post",
"parameters": [
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpsertRequest" } } }
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpsertResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/data/delete": {
"delete": {
"tags": ["Data Import"],
"summary": "删除翻译数据",
"description": "通过主键ID或过滤表达式删除翻译数据。\n\n**请求示例:**\n\n通过ID删除单条记录:\n```json\n{\n \"ids\": [123]\n}\n```\n\n通过ID删除多条记录:\n```json\n{\n \"ids\": [123, 456, 789],\n \"partition_name\": \"aviation_engine\"\n}\n```\n\n通过过滤表达式删除:\n```json\n{\n \"filter_expr\": \"text like 'aircraft%'\",\n \"partition_name\": \"aviation_engine\"\n}\n```\n```json\n{\n \"filter_expr\": \"translation == \"新的导航系统\"\",\n \"partition_name\": \"aviation_engine\"\n}\n```\n\n基于时间的过滤条件:\n```json\n{\n \"filter_expr\": \"create_time > 1640995200\",\n \"partition_name\": \"aviation_engine\"\n}\n```\n\n```json\n{\n \"filter_expr\": \"update_time >= 1672531200 and update_time <= 1675209600\"\n}\n```\n\n复杂过滤条件(时间+内容):\n```json\n{\n \"filter_expr\": \"create_time > 1640995200 and text like 'engine%'\"\n}\n```\n\n```json\n{\n \"filter_expr\": \"(update_time > 1672531200 or create_time > 1675209600) and translation like '%系统%'\"\n}\n```",
"operationId": "delete_translations_data_delete_delete",
"parameters": [
{
"name": "collection_name",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "集合名称", "default": "aviation_translations", "title": "Collection Name" },
"description": "集合名称"
},
{
"name": "uri",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "Milvus地址", "default": "http://124.71.148.16:19530", "title": "Uri" },
"description": "Milvus地址"
},
{
"name": "token",
"in": "query",
"required": false,
"schema": { "type": "string", "description": "认证token", "default": "root:Milvus", "title": "Token" },
"description": "认证token"
}
],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteRequest" } } }
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/tools/datetime-to-timestamp": {
"post": {
"tags": ["Tools"],
"summary": "日期时间转时间戳",
"description": "将日期时间字符串转换为Unix时间戳(秒)。\n\n**支持的日期时间格式:**\n- \"2023-01-01\"\n- \"2023-01-01 12:30:00\"\n- \"2023-01-01T12:30:00\"\n- \"2023-01-01T12:30:00Z\"\n- \"2023-01-01T12:30:00+08:00\"\n\n**请求示例:**\n\n基本日期转换:\n```json\n{\n \"datetime_str\": \"2023-01-01\"\n}\n```\n\n带时区的日期时间转换:\n```json\n{\n \"datetime_str\": \"2023-01-01 12:30:00\",\n \"timezone_offset\": \"+08:00\"\n}\n```\n\nISO格式转换:\n```json\n{\n \"datetime_str\": \"2023-01-01T12:30:00Z\"\n}\n```",
"operationId": "datetime_to_timestamp_tools_datetime_to_timestamp_post",
"requestBody": {
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/DateTimeToTimestampRequest" } } },
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/DateTimeToTimestampResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/tools/timestamp-to-datetime": {
"post": {
"tags": ["Tools"],
"summary": "时间戳转日期时间",
"description": "将Unix时间戳(秒)转换为日期时间字符串。\n\n**请求示例:**\n\n基本时间戳转换:\n```json\n{\n \"timestamp\": 1672531200\n}\n```\n\n指定时区的转换:\n```json\n{\n \"timestamp\": 1672531200,\n \"timezone_offset\": \"+08:00\"\n}\n```\n\n**响应说明:**\n- datetime_str: 人类可读的日期时间字符串\n- iso_format: ISO 8601标准格式\n- timezone_used: 使用的时区",
"operationId": "timestamp_to_datetime_tools_timestamp_to_datetime_post",
"requestBody": {
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/TimestampToDateTimeRequest" } } },
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/TimestampToDateTimeResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/tools/time-range-to-filter": {
"post": {
"tags": ["Tools"],
"summary": "时间范围转过滤表达式",
"description": "将日期范围转换为Milvus过滤表达式。\n\n**请求示例:**\n\n基本日期范围:\n```json\n{\n \"start_date\": \"2023-01-01\",\n \"end_date\": \"2023-01-31\"\n}\n```\n\n带时区的日期范围:\n```json\n{\n \"start_date\": \"2023-01-01 00:00:00\",\n \"end_date\": \"2023-01-31 23:59:59\",\n \"timezone_offset\": \"+08:00\"\n}\n```\n\n**用途:**\n生成的过滤表达式可直接用于delete API的filter_expr参数。",
"operationId": "time_range_to_filter_tools_time_range_to_filter_post",
"requestBody": {
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/TimeRangeRequest" } } },
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/TimeRangeResponse" } } }
},
"422": {
"description": "Validation Error",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
}
}
}
},
"/tools/filter-examples": {
"get": {
"tags": ["Tools"],
"summary": "获取时间过滤示例",
"description": "获取常用的时间过滤表达式示例和时间戳对照表。\n\n**返回内容:**\n- 常用过滤表达式示例\n- 重要时间点的时间戳对照表\n- 使用技巧和注意事项\n\n**用途:**\n帮助用户快速了解和使用时间过滤功能。",
"operationId": "get_filter_examples_tools_filter_examples_get",
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/FilterExamplesResponse" } } }
}
}
}
}
},
"components": {
"schemas": {
"AlterFieldRequest": {
"properties": {
"field_name": { "type": "string", "title": "Field Name", "default": "text" },
"max_length": { "anyOf": [{ "type": "integer" }, { "type": "null" }], "title": "Max Length" },
"max_capacity": { "anyOf": [{ "type": "integer" }, { "type": "null" }], "title": "Max Capacity" },
"mmap_enabled": { "anyOf": [{ "type": "boolean" }, { "type": "null" }], "title": "Mmap Enabled" }
},
"type": "object",
"title": "AlterFieldRequest"
},
"AlterFieldResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"message": { "type": "string", "title": "Message" },
"collection_name": { "type": "string", "title": "Collection Name" },
"field_name": { "type": "string", "title": "Field Name" }
},
"type": "object",
"required": ["success", "message", "collection_name", "field_name"],
"title": "AlterFieldResponse"
},
"Body_import_data_data_import_post": {
"properties": { "file": { "type": "string", "format": "binary", "title": "File", "description": "包含翻译数据的Excel文件" } },
"type": "object",
"required": ["file"],
"title": "Body_import_data_data_import_post"
},
"CreateCollectionResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"message": { "type": "string", "title": "Message" },
"collection_name": { "type": "string", "title": "Collection Name" }
},
"type": "object",
"required": ["success", "message", "collection_name"],
"title": "CreateCollectionResponse"
},
"CreatePartitionRequest": {
"properties": { "partition_name": { "type": "string", "title": "Partition Name", "default": "test_partition" } },
"type": "object",
"title": "CreatePartitionRequest"
},
"CreatePartitionResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"message": { "type": "string", "title": "Message" },
"collection_name": { "type": "string", "title": "Collection Name" },
"partition_name": { "type": "string", "title": "Partition Name" }
},
"type": "object",
"required": ["success", "message", "collection_name", "partition_name"],
"title": "CreatePartitionResponse"
},
"DateTimeToTimestampRequest": {
"properties": {
"datetime_str": { "type": "string", "title": "Datetime Str", "default": "2023-01-01 12:00:00" },
"timezone_offset": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Timezone Offset" }
},
"type": "object",
"title": "DateTimeToTimestampRequest"
},
"DateTimeToTimestampResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"datetime_str": { "type": "string", "title": "Datetime Str" },
"timestamp": { "type": "integer", "title": "Timestamp" },
"timezone_used": { "type": "string", "title": "Timezone Used" }
},
"type": "object",
"required": ["success", "datetime_str", "timestamp", "timezone_used"],
"title": "DateTimeToTimestampResponse"
},
"DeletePartitionRequest": {
"properties": { "partition_name": { "type": "string", "title": "Partition Name", "default": "test_partition" } },
"type": "object",
"title": "DeletePartitionRequest"
},
"DeletePartitionResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"message": { "type": "string", "title": "Message" },
"collection_name": { "type": "string", "title": "Collection Name" },
"partition_name": { "type": "string", "title": "Partition Name" }
},
"type": "object",
"required": ["success", "message", "collection_name", "partition_name"],
"title": "DeletePartitionResponse"
},
"DeleteRequest": {
"properties": {
"ids": {
"anyOf": [{ "items": { "anyOf": [{ "type": "string" }, { "type": "integer" }] }, "type": "array" }, { "type": "null" }],
"title": "Ids"
},
"filter_expr": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Filter Expr" },
"partition_name": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Partition Name" }
},
"type": "object",
"title": "DeleteRequest"
},
"DeleteResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"message": { "type": "string", "title": "Message" },
"delete_count": { "type": "integer", "title": "Delete Count" },
"collection_name": { "type": "string", "title": "Collection Name" }
},
"type": "object",
"required": ["success", "message", "delete_count", "collection_name"],
"title": "DeleteResponse"
},
"DescribeCollectionResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"collection_name": { "type": "string", "title": "Collection Name" },
"description": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Description" },
"fields": { "items": { "additionalProperties": true, "type": "object" }, "type": "array", "title": "Fields" },
"indexes": { "items": { "additionalProperties": true, "type": "object" }, "type": "array", "title": "Indexes" },
"load_status": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Load Status" },
"aliases": { "items": { "type": "string" }, "type": "array", "title": "Aliases", "default": [] },
"properties": { "additionalProperties": true, "type": "object", "title": "Properties", "default": {} }
},
"type": "object",
"required": ["success", "collection_name", "fields", "indexes"],
"title": "DescribeCollectionResponse"
},
"FilterExamplesResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"examples": { "additionalProperties": { "type": "string" }, "type": "object", "title": "Examples" },
"common_timestamps": { "additionalProperties": { "type": "integer" }, "type": "object", "title": "Common Timestamps" },
"usage_tips": { "items": { "type": "string" }, "type": "array", "title": "Usage Tips" }
},
"type": "object",
"required": ["success", "examples", "common_timestamps", "usage_tips"],
"title": "FilterExamplesResponse"
},
"HTTPValidationError": {
"properties": { "detail": { "items": { "$ref": "#/components/schemas/ValidationError" }, "type": "array", "title": "Detail" } },
"type": "object",
"title": "HTTPValidationError"
},
"ImportDataResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"message": { "type": "string", "title": "Message" },
"data_count": { "type": "integer", "title": "Data Count" },
"time_elapsed": { "type": "number", "title": "Time Elapsed" }
},
"type": "object",
"required": ["success", "message", "data_count", "time_elapsed"],
"title": "ImportDataResponse"
},
"InsertSingleRequest": {
"properties": {
"text": { "type": "string", "title": "Text", "default": "aircraft engine" },
"translation": { "type": "string", "title": "Translation", "default": "航空发动机" },
"partition_name": { "type": "string", "title": "Partition Name", "default": "from_api" }
},
"type": "object",
"title": "InsertSingleRequest"
},
"InsertSingleResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"message": { "type": "string", "title": "Message" },
"collection_name": { "type": "string", "title": "Collection Name" },
"partition_name": { "type": "string", "title": "Partition Name" }
},
"type": "object",
"required": ["success", "message", "collection_name", "partition_name"],
"title": "InsertSingleResponse"
},
"LoadPartitionResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"message": { "type": "string", "title": "Message" },
"collection_name": { "type": "string", "title": "Collection Name" },
"partition_name": { "type": "string", "title": "Partition Name" }
},
"type": "object",
"required": ["success", "message", "collection_name", "partition_name"],
"title": "LoadPartitionResponse"
},
"SearchRequest": {
"properties": {
"query_text": { "type": "string", "title": "Query Text", "default": "aircraft" },
"search_type": { "type": "string", "title": "Search Type", "default": "full_text" },
"search_direction": { "type": "string", "title": "Search Direction", "default": "en_to_zh" },
"partition_names": { "anyOf": [{ "items": {}, "type": "array" }, { "type": "null" }], "title": "Partition Names" },
"limit": { "type": "integer", "title": "Limit", "default": 5 }
},
"type": "object",
"title": "SearchRequest"
},
"SearchResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"results": { "items": { "$ref": "#/components/schemas/SearchResult" }, "type": "array", "title": "Results" },
"count": { "type": "integer", "title": "Count" }
},
"type": "object",
"required": ["success", "results", "count"],
"title": "SearchResponse"
},
"SearchResult": {
"properties": {
"id": { "anyOf": [{ "type": "integer" }, { "type": "null" }], "title": "Id" },
"text": { "type": "string", "title": "Text" },
"translation": { "type": "string", "title": "Translation" },
"score": { "anyOf": [{ "type": "number" }, { "type": "null" }], "title": "Score" },
"fuzzy_score": { "anyOf": [{ "type": "number" }, { "type": "null" }], "title": "Fuzzy Score" },
"partition_name": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Partition Name" },
"created_at": { "anyOf": [{ "type": "string", "format": "date-time" }, { "type": "null" }], "title": "Created At" },
"updated_at": { "anyOf": [{ "type": "string", "format": "date-time" }, { "type": "null" }], "title": "Updated At" }
},
"type": "object",
"required": ["text", "translation"],
"title": "SearchResult"
},
"TimeRangeRequest": {
"properties": {
"start_date": { "type": "string", "title": "Start Date", "default": "2023-01-01" },
"end_date": { "type": "string", "title": "End Date", "default": "2023-01-31" },
"timezone_offset": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Timezone Offset" }
},
"type": "object",
"title": "TimeRangeRequest"
},
"TimeRangeResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"start_date": { "type": "string", "title": "Start Date" },
"end_date": { "type": "string", "title": "End Date" },
"start_timestamp": { "type": "integer", "title": "Start Timestamp" },
"end_timestamp": { "type": "integer", "title": "End Timestamp" },
"filter_expression": { "type": "string", "title": "Filter Expression" },
"timezone_used": { "type": "string", "title": "Timezone Used" }
},
"type": "object",
"required": ["success", "start_date", "end_date", "start_timestamp", "end_timestamp", "filter_expression", "timezone_used"],
"title": "TimeRangeResponse"
},
"TimestampToDateTimeRequest": {
"properties": {
"timestamp": { "type": "integer", "title": "Timestamp", "default": 1672531200 },
"timezone_offset": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Timezone Offset" }
},
"type": "object",
"title": "TimestampToDateTimeRequest"
},
"TimestampToDateTimeResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"timestamp": { "type": "integer", "title": "Timestamp" },
"datetime_str": { "type": "string", "title": "Datetime Str" },
"iso_format": { "type": "string", "title": "Iso Format" },
"timezone_used": { "type": "string", "title": "Timezone Used" }
},
"type": "object",
"required": ["success", "timestamp", "datetime_str", "iso_format", "timezone_used"],
"title": "TimestampToDateTimeResponse"
},
"UpsertRequest": {
"properties": {
"data": {
"anyOf": [
{ "additionalProperties": true, "type": "object" },
{ "items": { "additionalProperties": true, "type": "object" }, "type": "array" }
],
"title": "Data"
},
"partition_name": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Partition Name" },
"use_content_based_id": {
"anyOf": [{ "type": "boolean" }, { "type": "null" }],
"title": "Use Content Based Id",
"default": false
}
},
"type": "object",
"required": ["data"],
"title": "UpsertRequest"
},
"UpsertResponse": {
"properties": {
"success": { "type": "boolean", "title": "Success" },
"message": { "type": "string", "title": "Message" },
"upsert_count": { "type": "integer", "title": "Upsert Count" },
"ids": { "anyOf": [{ "items": { "type": "integer" }, "type": "array" }, { "type": "null" }], "title": "Ids" },
"collection_name": { "type": "string", "title": "Collection Name" }
},
"type": "object",
"required": ["success", "message", "upsert_count", "collection_name"],
"title": "UpsertResponse"
},
"ValidationError": {
"properties": {
"loc": { "items": { "anyOf": [{ "type": "string" }, { "type": "integer" }] }, "type": "array", "title": "Location" },
"msg": { "type": "string", "title": "Message" },
"type": { "type": "string", "title": "Error Type" }
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError"
}
}
},
"tags": [
{ "name": "Collection Management", "description": "集合管理相关操作" },
{ "name": "Data Import", "description": "数据导入相关操作" },
{ "name": "Search", "description": "数据搜索相关操作" },
{ "name": "Tools", "description": "实用工具接口" }
]
}
......@@ -22,11 +22,11 @@ const props = defineProps({
/** 绑定的值 (v-model) */
value: {
type: [Array, String] as PropType<any[] | string | null | undefined>,
default: () => []
default: undefined
},
modelValue: {
type: [Array, String] as PropType<any[] | string | null | undefined>,
default: () => []
default: undefined
},
/** 静态选项 */
options: {
......
......@@ -235,6 +235,18 @@
</div>
<n-switch v-model:value="appStore.collapsed" />
</div>
<n-divider class="my-4" />
<div class="text-sm font-semibold mb-3" :style="{ color: themeVars.textColor2 }">编辑器设置</div>
<!-- 自动展开新增节点 -->
<div class="flex items-center justify-between py-3" :style="{ borderColor: themeVars.dividerColor }">
<div>
<div class="text-sm" :style="{ color: themeVars.textColor1 }">插入节点自动展开</div>
<div class="text-xs mt-0.5" :style="{ color: themeVars.textColor3 }">插入/粘贴节点及表格行列时,自动展开相关节点</div>
</div>
<n-switch v-model:value="appStore.autoExpandOnInsert" />
</div>
</div>
</n-tab-pane>
......@@ -395,6 +407,7 @@ const resetPrefs = () => {
appStore.fixedHeader = false
appStore.collapsed = false
appStore.transitionName = 'none'
appStore.autoExpandOnInsert = true
customColor.value = '#165DFF'
applyAndSave()
window.$message.success('已重置为默认设置')
......@@ -409,7 +422,8 @@ const copyPrefs = async () => {
isDark: appStore.isDark,
colorWeak: appStore.colorWeak,
grayMode: appStore.grayMode,
transitionName: appStore.transitionName
transitionName: appStore.transitionName,
autoExpandOnInsert: appStore.autoExpandOnInsert
},
null,
2
......@@ -439,6 +453,7 @@ const importPrefs = async () => {
if (typeof conf.colorWeak === 'boolean') appStore.colorWeak = conf.colorWeak
if (typeof conf.grayMode === 'boolean') appStore.grayMode = conf.grayMode
if (conf.transitionName) appStore.transitionName = conf.transitionName
if (typeof conf.autoExpandOnInsert === 'boolean') appStore.autoExpandOnInsert = conf.autoExpandOnInsert
applyAndSave()
window.$message.success('偏好设置已成功导入并应用')
......
......@@ -19,7 +19,8 @@ export const useAppStore = defineStore('app', {
loadingText: '加载中...',
transitionName: 'none',
settingsPinned: false,
settingsOpen: false
settingsOpen: false,
autoExpandOnInsert: true
}),
actions: {
toggleSidebar() {
......
......@@ -19,4 +19,5 @@ export interface AppState {
transitionName: string
settingsPinned: boolean
settingsOpen: boolean
autoExpandOnInsert: boolean // 新增/粘贴节点及表格行列时自动展开
}
import { markRaw, type Ref } from 'vue'
import { markRaw, nextTick, type Ref } from 'vue'
import type { XmlNode } from '@/types/xmlNode'
import type { EditorState } from './types'
import { createDefaultAttributes, canAddChild } from '@/utils/dtdManager'
import { parseXmlToTree } from '@/utils/xmlParser'
import { useAppStore } from '@/store/app'
export const nodeSelectedRefs = new Map<string, Ref<boolean>>()
/**
* 辅助函数:根据标签名创建具有完整子结构的默认 XML 节点对象
*/
export const createTableStructure = (rows: number, cols: number, cellChildTags: string[]): XmlNode => {
const tableId = crypto.randomUUID()
const tgroupId = crypto.randomUUID()
const theadId = crypto.randomUUID()
const tbodyId = crypto.randomUUID()
// 1. 生成 COLSPEC
const colspecs: XmlNode[] = []
for (let i = 1; i <= cols; i++) {
colspecs.push({
id: crypto.randomUUID(),
tagName: 'COLSPEC',
attributes: {
COLNAME: `col${i}`,
COLNUM: i.toString(),
COLWIDTH: '1*'
},
children: [],
textContent: '',
mixedContent: [],
parentId: tgroupId
})
}
// 辅助函数:根据 cellChildTags 为 ENTRY 节点填充子段落
const populateEntryChildren = (entryId: string, cellChildTags: string[], isHeader = false) => {
const entryChildren: XmlNode[] = []
const entryMixed: any[] = []
if (cellChildTags && cellChildTags.length > 0) {
// 按照 PARAC -> PARA 的顺序插入,以确保 PARAC 始终在 PARA 节点的前面
const order = ['PARAC', 'PARA']
for (const tag of order) {
if (cellChildTags.includes(tag)) {
const childId = crypto.randomUUID()
entryChildren.push({
id: childId,
tagName: tag,
attributes: {},
children: [],
textContent: '',
mixedContent: [],
parentId: entryId
})
entryMixed.push({
type: 'element',
nodeId: childId
})
}
}
}
return { entryChildren, entryMixed }
}
// 2. 生成 THEAD
const headRowId = crypto.randomUUID()
const headEntries: XmlNode[] = []
for (let c = 0; c < cols; c++) {
const entryId = crypto.randomUUID()
const { entryChildren, entryMixed } = populateEntryChildren(entryId, cellChildTags, true)
headEntries.push({
id: entryId,
tagName: 'ENTRY',
attributes: {},
children: entryChildren,
textContent: entryChildren.length > 0 ? '' : `列头 ${c + 1}`,
mixedContent: entryChildren.length > 0 ? entryMixed : [],
parentId: headRowId
})
}
const theadNode: XmlNode = {
id: theadId,
tagName: 'THEAD',
attributes: {},
children: [
{
id: headRowId,
tagName: 'ROW',
attributes: {},
children: headEntries,
textContent: '',
mixedContent: [],
parentId: theadId
}
],
textContent: '',
mixedContent: [],
parentId: tgroupId
}
// 3. 生成 TBODY 行
const tbodyRows: XmlNode[] = []
for (let r = 0; r < rows; r++) {
const rowId = crypto.randomUUID()
const entries: XmlNode[] = []
for (let c = 0; c < cols; c++) {
const entryId = crypto.randomUUID()
const { entryChildren, entryMixed } = populateEntryChildren(entryId, cellChildTags, false)
entries.push({
id: entryId,
tagName: 'ENTRY',
attributes: {},
children: entryChildren,
textContent: entryChildren.length > 0 ? '' : `内容 ${r + 1}-${c + 1}`,
mixedContent: entryChildren.length > 0 ? entryMixed : [],
parentId: rowId
})
}
tbodyRows.push({
id: rowId,
tagName: 'ROW',
attributes: {},
children: entries,
textContent: '',
mixedContent: [],
parentId: tbodyId
})
}
const tgroupNode: XmlNode = {
id: tgroupId,
tagName: 'TGROUP',
attributes: { COLS: cols.toString() },
children: [
...colspecs,
theadNode,
{
id: tbodyId,
tagName: 'TBODY',
attributes: {},
children: tbodyRows,
textContent: '',
mixedContent: [],
parentId: tgroupId
}
],
textContent: '',
mixedContent: [],
parentId: tableId
}
return {
id: tableId,
tagName: 'TABLE',
attributes: {},
children: [tgroupNode],
textContent: '',
mixedContent: [],
parentId: null
}
}
const createDefaultNodeStructure = (tagName: string): XmlNode => {
const id = crypto.randomUUID()
const attributes = createDefaultAttributes(tagName)
if (tagName === 'TABLE') {
const tgroupId = crypto.randomUUID()
const theadId = crypto.randomUUID()
const tbodyId = crypto.randomUUID()
const rowId1 = crypto.randomUUID()
const rowId2 = crypto.randomUUID()
return {
id,
tagName: 'TABLE',
attributes: {},
children: [
{
id: tgroupId,
tagName: 'TGROUP',
attributes: { COLS: '3' },
children: [
{
id: crypto.randomUUID(),
tagName: 'COLSPEC',
attributes: { COLNAME: 'col1', COLNUM: '1', COLWIDTH: '1*' },
children: [],
textContent: '',
mixedContent: [],
parentId: tgroupId
},
{
id: crypto.randomUUID(),
tagName: 'COLSPEC',
attributes: { COLNAME: 'col2', COLNUM: '2', COLWIDTH: '1*' },
children: [],
textContent: '',
mixedContent: [],
parentId: tgroupId
},
{
id: crypto.randomUUID(),
tagName: 'COLSPEC',
attributes: { COLNAME: 'col3', COLNUM: '3', COLWIDTH: '1*' },
children: [],
textContent: '',
mixedContent: [],
parentId: tgroupId
},
{
id: theadId,
tagName: 'THEAD',
attributes: {},
children: [
{
id: rowId1,
tagName: 'ROW',
attributes: {},
children: [
{
id: crypto.randomUUID(),
tagName: 'ENTRY',
attributes: {},
children: [],
textContent: '列头 1',
mixedContent: [],
parentId: rowId1
},
{
id: crypto.randomUUID(),
tagName: 'ENTRY',
attributes: {},
children: [],
textContent: '列头 2',
mixedContent: [],
parentId: rowId1
},
{
id: crypto.randomUUID(),
tagName: 'ENTRY',
attributes: {},
children: [],
textContent: '列头 3',
mixedContent: [],
parentId: rowId1
}
],
textContent: '',
mixedContent: [],
parentId: theadId
}
],
textContent: '',
mixedContent: [],
parentId: tgroupId
},
{
id: tbodyId,
tagName: 'TBODY',
attributes: {},
children: [
{
id: rowId2,
tagName: 'ROW',
attributes: {},
children: [
{
id: crypto.randomUUID(),
tagName: 'ENTRY',
attributes: {},
children: [],
textContent: '内容 1-1',
mixedContent: [],
parentId: rowId2
},
{
id: crypto.randomUUID(),
tagName: 'ENTRY',
attributes: {},
children: [],
textContent: '内容 1-2',
mixedContent: [],
parentId: rowId2
},
{
id: crypto.randomUUID(),
tagName: 'ENTRY',
attributes: {},
children: [],
textContent: '内容 1-3',
mixedContent: [],
parentId: rowId2
}
],
textContent: '',
mixedContent: [],
parentId: tbodyId
}
],
textContent: '',
mixedContent: [],
parentId: tgroupId
}
],
textContent: '',
mixedContent: [],
parentId: id
}
],
textContent: '',
mixedContent: [],
parentId: null
}
return createTableStructure(3, 3, ['PARAC', 'PARA'])
}
if (tagName === 'GRAPHIC') {
......@@ -300,7 +308,9 @@ export const useEditorStore = defineStore('editor', {
undoStack: [],
redoStack: [],
lastUndoRedoTime: 0,
editorZoom: 100
editorZoom: 100,
expandedKeys: [],
skipExpandOnSelect: false
}),
getters: {
......@@ -315,6 +325,36 @@ export const useEditorStore = defineStore('editor', {
},
actions: {
setExpandedKeys(keys: string[]) {
this.expandedKeys = keys
},
expandNodes(ids: string[]) {
const set = new Set(this.expandedKeys)
ids.forEach((id) => set.add(id))
this.expandedKeys = Array.from(set)
},
handleNodeInsertion(node: XmlNode) {
const appStore = useAppStore()
if (appStore.autoExpandOnInsert) {
const collectKeys = (n: XmlNode): string[] => {
const keys: string[] = []
if (n.children && n.children.length > 0) {
keys.push(n.id)
n.children.forEach((c) => {
keys.push(...collectKeys(c))
})
}
return keys
}
const ids = collectKeys(node)
if (ids.length > 0) {
this.expandNodes(ids)
}
}
},
rebuildNodeMap() {
const map = new Map<string, { node: XmlNode; parent: XmlNode | null }>()
if (this.xmlTree) {
......@@ -329,7 +369,7 @@ export const useEditorStore = defineStore('editor', {
this.nodeMap = markRaw(map)
},
insertNode(tagName: string, insertBelow: boolean) {
insertNode(tagName: string, insertBelow: boolean, customNode?: XmlNode) {
if (!this.xmlTree || !this.selectedNodeId) {
window.$message.warning('请先在树中选择一个目标节点!')
return
......@@ -338,7 +378,33 @@ export const useEditorStore = defineStore('editor', {
const selected = this.selectedNode
if (!selected) return
const newNode = createDefaultNodeStructure(tagName)
// 1. DTD 规则约束校验
let parentTag = ''
let parentNode: XmlNode | null = null
if (insertBelow) {
parentNode = this.selectedNodeParent
if (!parentNode) {
window.$message.warning('无法在根节点下方插入兄弟节点')
return
}
parentTag = parentNode.tagName
} else {
parentNode = selected
parentTag = selected.tagName
}
const existingCount = parentNode.children.filter((c) => c.tagName === tagName).length
if (!canAddChild(parentTag, tagName, existingCount)) {
window.$message.warning(`DTD 校验失败: 节点 <${parentTag}> 无法接受子元素 <${tagName}>`)
return
}
const appStore = useAppStore()
if (!appStore.autoExpandOnInsert) {
this.skipExpandOnSelect = true
}
const newNode = customNode || createDefaultNodeStructure(tagName)
const setParentRecursive = (n: XmlNode, pid: string) => {
n.parentId = pid
......@@ -371,7 +437,14 @@ export const useEditorStore = defineStore('editor', {
this.selectedNodeId = newNode.id
window.$message.success(`成功向节点内插入子节点 <${tagName}>`)
}
this.handleNodeInsertion(newNode)
this.rebuildNodeMap()
if (!appStore.autoExpandOnInsert) {
nextTick(() => {
this.skipExpandOnSelect = false
})
}
},
insertXmlFragment(xmlString: string, mode: 'above' | 'below' | 'inside', targetNodeId?: string) {
......@@ -380,6 +453,11 @@ export const useEditorStore = defineStore('editor', {
throw new Error('请先选择一个目标节点!')
}
const appStore = useAppStore()
if (!appStore.autoExpandOnInsert) {
this.skipExpandOnSelect = true
}
const item = this.nodeMap.get(targetId)
if (!item) {
throw new Error('当前的目标节点无效!')
......@@ -453,7 +531,14 @@ export const useEditorStore = defineStore('editor', {
}
this.selectedNodeId = newNodes[newNodes.length - 1].id
newNodes.forEach((node) => this.handleNodeInsertion(node))
this.rebuildNodeMap()
if (!appStore.autoExpandOnInsert) {
nextTick(() => {
this.skipExpandOnSelect = false
})
}
return newNodes.length
},
......
......@@ -14,4 +14,6 @@ export interface EditorState {
redoStack: Snapshot[] // 保存 Snapshot 重做快照
lastUndoRedoTime: number // 回退重做动作的时间戳,用于触发视口重定位
editorZoom: number // 编辑器文字字号缩放比例 (80 - 200)
expandedKeys: string[] // 全局的节点展开状态
skipExpandOnSelect: boolean // 在选中时是否跳过祖先展开逻辑
}
export interface TranslateXmlOptions {
database_partition_name?: string | null
model_name?: string
search_direction?: 'en_to_zh' | 'zh_to_en'
target_tags?: string
suffix?: string
save_to_database?: boolean
use_parallel_method?: boolean
enable_nested_translation?: boolean
skip_tags?: string
}
export interface TranslateXmlResponse {
success: boolean
message: string
file_id: string
}
export interface TranslationStatusResponse {
status: 'pending' | 'processing' | 'completed' | 'failed'
progress: number
message?: string
error?: string
download_url?: string
}
import { type TranslateXmlOptions, type TranslateXmlResponse, type TranslationStatusResponse } from '../constants'
import { useEditorStore } from '@/store/editor'
import { serializeTreeToXml, parseXmlToTreeAsync } from '@/utils/xmlParser'
import { getBaseOrigin } from '@/utils/render'
export function useBatchTranslate() {
const editorStore = useEditorStore()
const showModal = ref(false)
const loading = ref(false)
const completed = ref(false)
const progress = ref(0)
const progressText = ref('正在准备翻译文档...')
const resultFileId = ref('')
const resultDownloadUrl = ref('')
const availableTags = ref<string[]>(['PARAC', 'TITLEC'])
const availableTagOptions = computed(() => {
return availableTags.value.map((tag) => ({ label: tag, value: tag }))
})
const form = ref({
model_name: 'glm-4-flash',
search_direction: 'en_to_zh',
target_tags: ['PARAC', 'TITLEC'],
suffix: '',
use_parallel_method: true,
save_to_database: false,
database_partition_name: import.meta.env.VITE_PARTITION_NAME
})
const modelOptions = [
{ label: 'GLM 4 Flash (速度快,经济)', value: 'glm-4-flash' },
{ label: 'DeepSeek Chat (高质量翻译)', value: 'deepseek-chat' },
{ label: 'GLM 4 Air (轻量高效)', value: 'glm-4-air' }
]
const directionOptions = [
{ label: '英译中 (EN -> ZH)', value: 'en_to_zh' },
{ label: '中译英 (ZH -> EN)', value: 'zh_to_en' }
]
const statusLabel = computed(() => {
if (loading.value) return '翻译中'
if (completed.value) return '已完成'
return '未开始'
})
const statusType = computed(() => {
if (loading.value) return 'info'
if (completed.value) return 'success'
return 'default'
})
const reset = () => {
loading.value = false
completed.value = false
progress.value = 0
progressText.value = '正在准备翻译文档...'
resultFileId.value = ''
resultDownloadUrl.value = ''
if (pollInterval) {
clearInterval(pollInterval)
pollInterval = null
}
}
const open = async () => {
showModal.value = true
if (loading.value || completed.value) {
return
}
reset()
try {
const res = (await service.get('/xml/tags')) as any as { success: boolean; message: string; tags: string[] }
if (res && res.success && res.tags && res.tags.length > 0) {
availableTags.value = res.tags
// 自动勾选所有获取到的标签
form.value.target_tags = [...res.tags]
}
} catch (e) {
console.error('获取可翻译标签失败:', e)
}
}
let pollInterval: any = null
const startPolling = (fileId: string) => {
if (pollInterval) clearInterval(pollInterval)
pollInterval = setInterval(async () => {
try {
const statusRes = (await service.get(`/translation/status/${fileId}`)) as any as TranslationStatusResponse
if (statusRes) {
if (statusRes.status === 'completed') {
clearInterval(pollInterval)
pollInterval = null
progress.value = 100
progressText.value = '翻译完成!'
loading.value = false
completed.value = true
// 构造下载链接
let dlUrl = statusRes.download_url
if (dlUrl) {
if (dlUrl.startsWith('/') && !dlUrl.startsWith('/translations')) {
dlUrl = `/translations${dlUrl}`
}
dlUrl = `${getBaseOrigin()}${dlUrl}`
}
resultDownloadUrl.value = dlUrl || `${getBaseOrigin()}/translations/download/xml/${fileId}`
} else if (statusRes.status === 'failed') {
clearInterval(pollInterval)
pollInterval = null
loading.value = false
window.$message.error(`翻译失败: ${statusRes.error || statusRes.message || '服务器内部错误'}`)
} else {
// 更新进度
const currentProgress = statusRes.progress || 0
progress.value = Math.max(30, Math.min(95, Math.floor(currentProgress)))
progressText.value = statusRes.message || '翻译正在进行中...'
}
}
} catch (e) {
console.error('轮询状态异常:', e)
}
}, 1500)
}
const startTranslation = async () => {
if (!editorStore.xmlTree) {
window.$message.error('编辑器中无 XML 数据')
return
}
loading.value = true
progress.value = 5
progressText.value = '正在序列化当前 XML...'
try {
const xmlContent = serializeTreeToXml(editorStore.xmlTree, 0, true)
const blob = new Blob([xmlContent], { type: 'application/xml;charset=utf-8;' })
progress.value = 15
progressText.value = '正在上传文件至翻译服务器...'
const formData = new FormData()
formData.append('files', blob, 'document.xml')
if (form.value.model_name) {
formData.append('model_name', form.value.model_name)
}
formData.append('search_direction', form.value.search_direction)
formData.append('target_tags', form.value.target_tags.join(','))
if (form.value.suffix) {
formData.append('suffix', form.value.suffix)
}
formData.append('save_to_database', String(form.value.save_to_database))
formData.append('use_parallel_method', String(form.value.use_parallel_method))
const dbPartition = form.value.save_to_database ? form.value.database_partition_name : undefined
if (dbPartition) {
formData.append('database_partition_name', dbPartition)
}
const res = (await service.post('/translate/xml', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})) as any as TranslateXmlResponse
if (res && res.success && res.file_id) {
resultFileId.value = res.file_id
progress.value = 30
progressText.value = '翻译任务已创建,正在排队等待处理...'
// 开始轮询翻译状态
startPolling(res.file_id)
} else {
throw new Error(res.message || '创建翻译任务失败')
}
} catch (err: any) {
loading.value = false
window.$message.error(`翻译出错: ${err.message || err}`)
}
}
const downloadResult = () => {
if (!resultFileId.value) return
const url = resultDownloadUrl.value || `${getBaseOrigin()}/translations/download/xml/${resultFileId.value}`
const link = document.createElement('a')
link.href = url
link.download = `translated_${Date.now()}.xml`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
const importResult = async () => {
if (!resultFileId.value) return
window.$loading?.show('获取并解析翻译后的 XML 文件...')
try {
const url = resultDownloadUrl.value || `${getBaseOrigin()}/translations/download/xml/${resultFileId.value}`
const res = await fetch(url)
if (!res.ok) throw new Error('获取文件失败')
const xmlText = await res.text()
const tree = await parseXmlToTreeAsync(xmlText)
editorStore.setXmlTree(tree)
window.$message.success('翻译后的 XML 已成功导入编辑器!')
showModal.value = false
} catch (err: any) {
window.$message.error(`导入失败: ${err.message || err}`)
} finally {
window.$loading?.hide()
}
}
onBeforeUnmount(() => {
if (pollInterval) {
clearInterval(pollInterval)
}
})
return {
showModal,
loading,
completed,
progress,
progressText,
resultFileId,
resultDownloadUrl,
availableTags,
availableTagOptions,
form,
modelOptions,
directionOptions,
statusLabel,
statusType,
open,
reset,
startTranslation,
downloadResult,
importResult
}
}
<template>
<CommonModal
v-model="showModal"
title="批量 XML 翻译"
:width="600"
:show-confirm="false"
:show-cancel="false"
>
<template #header-extra>
<n-tag :type="statusType" size="small">{{ statusLabel }}</n-tag>
</template>
<div class="py-2">
<n-form v-if="!loading && !completed" label-placement="left" label-width="120" size="medium">
<n-form-item label="翻译模型">
<CommonSelect v-model:value="form.model_name" :options="modelOptions" />
</n-form-item>
<n-form-item label="翻译方向">
<CommonSelect v-model:value="form.search_direction" :options="directionOptions" />
</n-form-item>
<n-form-item label="目标翻译标签">
<div class="flex flex-col gap-2 w-full">
<CommonCheckbox v-model:value="form.target_tags" :options="availableTagOptions" />
<span class="text-xs text-color3">仅翻译所勾选的 XML 节点标签</span>
</div>
</n-form-item>
<n-form-item label="翻译标签后缀">
<n-input v-model:value="form.suffix" placeholder="默认无后缀,如填 'Z' 则 PARA 翻译为 PARAZ" />
</n-form-item>
<n-form-item label="并行处理模式">
<n-space align="center">
<n-switch v-model:value="form.use_parallel_method" />
<span class="text-xs text-color3">开启并行模式可显著提升大型文件翻译速度</span>
</n-space>
</n-form-item>
<n-form-item label="保存翻译至 Milvus">
<n-space align="center">
<n-switch v-model:value="form.save_to_database" />
<span class="text-xs text-color3">将翻译对照结果保存到 Milvus 数据库以供以后检索</span>
</n-space>
</n-form-item>
<n-form-item v-if="form.save_to_database" label="Milvus 分区名">
<n-input v-model:value="form.database_partition_name" placeholder="请输入数据库分区名称" />
</n-form-item>
</n-form>
<!-- 翻译进行中/进度条 -->
<div v-else-if="loading" class="flex flex-col items-center justify-center py-6 gap-4">
<div class="text-sm font-semibold text-color1">{{ progressText }}</div>
<div class="w-full px-8">
<n-progress
type="line"
:percentage="progress"
:indicator-placement="'inside'"
processing
status="info"
/>
</div>
</div>
<!-- 翻译完成/结果展示 -->
<div v-else-if="completed" class="flex flex-col items-center justify-center py-6 gap-4 text-center">
<n-icon size="48" class="text-success">
<checkmark-circle-outline />
</n-icon>
<div class="text-base font-bold text-success">XML 文档批量翻译完成!</div>
<div class="text-xs text-color3">所有指定标签的文本已翻译成功。你可以直接导入当前编辑器或下载文件。</div>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-3 w-full">
<!-- 翻译中状态:允许隐入后台 -->
<template v-if="loading">
<CommonButton type="primary" secondary @click="showModal = false">
后台运行 (隐藏)
</CommonButton>
</template>
<!-- 翻译完成状态 -->
<template v-else-if="completed">
<CommonButton @click="reset">重新翻译</CommonButton>
<CommonButton type="warning" secondary @click="downloadResult">下载 XML 文件</CommonButton>
<CommonButton type="primary" @click="importResult">直接导入编辑器</CommonButton>
</template>
<!-- 未开始配置状态 -->
<template v-else>
<CommonButton @click="showModal = false">取消</CommonButton>
<CommonButton type="primary" @click="startTranslation">开始翻译</CommonButton>
</template>
</div>
</template>
</CommonModal>
</template>
<script setup lang="ts">
import { CheckmarkCircleOutline } from '@vicons/ionicons5'
import { useBatchTranslate } from './functionals'
const {
showModal,
loading,
completed,
progress,
progressText,
availableTags,
availableTagOptions,
form,
modelOptions,
directionOptions,
statusLabel,
statusType,
open,
reset,
startTranslation,
downloadResult,
importResult
} = useBatchTranslate()
defineExpose({
open,
loading,
progress,
progressText,
completed
})
</script>
<template>
<CommonModal v-model="show" title="插入表格" :width="380" @confirm="handleConfirm">
<n-form ref="formRef" :model="form" :rules="rules" label-placement="top">
<div class="flex flex-col gap-4 py-2">
<div class="grid grid-cols-2 gap-4">
<n-form-item label="行数" path="rows">
<CommonInputNumber
v-model:value="form.rows"
:min="1"
:max="50"
:step="1"
:strict="true"
:show-button="true"
class="w-full"
placeholder="请输入行数"
/>
</n-form-item>
<n-form-item label="列数" path="cols">
<CommonInputNumber
v-model:value="form.cols"
:min="1"
:max="20"
:step="1"
:strict="true"
:show-button="true"
class="w-full"
placeholder="请输入列数"
/>
</n-form-item>
</div>
<n-form-item label="单元格初始化段落节点" path="cellChildTags">
<div class="w-full bg-fill-2 p-3 rounded border border-divider">
<CommonCheckbox
v-model:value="form.cellChildTags"
:options="cellChildOptions"
:space-size="24"
/>
</div>
</n-form-item>
</div>
</n-form>
</CommonModal>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import type { FormInst, FormRules } from 'naive-ui'
import CommonCheckbox from '@/components/CommonCheckbox.vue'
const emit = defineEmits<{
confirm: [rows: number, cols: number, cellChildTags: string[]]
}>()
const cellChildOptions = [
{ label: 'PARAC (中文)', value: 'PARAC' },
{ label: 'PARA (英文)', value: 'PARA' }
]
const show = ref(false)
const formRef = ref<FormInst | null>(null)
const form = reactive({
rows: 3,
cols: 3,
cellChildTags: ['PARAC', 'PARA']
})
const rules: FormRules = {
rows: { type: 'number', required: true, message: '请指定行数', trigger: ['blur', 'change'] },
cols: { type: 'number', required: true, message: '请指定列数', trigger: ['blur', 'change'] }
}
const open = () => {
form.rows = 3
form.cols = 3
form.cellChildTags = ['PARAC', 'PARA']
show.value = true
}
const handleConfirm = async () => {
try {
await formRef.value?.validate()
emit('confirm', form.rows, form.cols, form.cellChildTags)
show.value = false
} catch (err) {
// validation failed
}
}
defineExpose({ open })
</script>
export interface ExtractOptions {
tag_pairs: Array<{ en_tag: string; cn_tag: string }>
save_excel?: boolean
import_to_db?: boolean
partition_name?: string
}
export interface ExtractResponse {
success: boolean
message: string
file_id: string
download_url?: string
total_count: number
import_count: number
preview_data: Array<{ text: string; translation: string }>
}
import { type ExtractResponse } from '../constants'
import { useEditorStore } from '@/store/editor'
import { serializeTreeToXml } from '@/utils/xmlParser'
import { getBaseOrigin } from '@/utils/render'
export function useExtractTranslate() {
const editorStore = useEditorStore()
const showModal = ref(false)
const loading = ref(false)
const completed = ref(false)
const totalCount = ref(0)
const importCount = ref(0)
const resultDownloadUrl = ref('')
const previewList = ref<any[]>([])
const form = ref({
save_excel: true,
import_to_db: false,
partition_name: import.meta.env.VITE_PARTITION_NAME,
tag_pairs: [
{ en_tag: 'PARA', cn_tag: 'PARAC' },
{ en_tag: 'TITLE', cn_tag: 'TITLEC' }
]
})
const previewColumns = [
{ title: '英文源文本 (text)', key: 'text', ellipsis: { tooltip: true } },
{ title: '中文翻译 (translation)', key: 'translation', ellipsis: { tooltip: true } }
]
const reset = () => {
loading.value = false
completed.value = false
totalCount.value = 0
importCount.value = 0
resultDownloadUrl.value = ''
previewList.value = []
}
const open = () => {
showModal.value = true
reset()
}
const addTagPair = () => {
form.value.tag_pairs.push({ en_tag: '', cn_tag: '' })
}
const removeTagPair = (index: number) => {
form.value.tag_pairs.splice(index, 1)
}
const startExtraction = async () => {
if (!editorStore.xmlTree) {
window.$message.error('编辑器中无 XML 数据')
return
}
// 过滤掉空的标签对
const validPairs = form.value.tag_pairs.filter((p) => p.en_tag.trim() && p.cn_tag.trim())
if (validPairs.length === 0) {
window.$message.warning('请配置至少一个有效的标签对')
return
}
loading.value = true
try {
const xmlContent = serializeTreeToXml(editorStore.xmlTree, 0, true)
const blob = new Blob([xmlContent], { type: 'application/xml;charset=utf-8;' })
const formData = new FormData()
formData.append('file', blob, 'document.xml')
formData.append('tag_pairs', JSON.stringify(validPairs))
if (form.value.save_excel !== undefined) {
formData.append('save_excel', String(form.value.save_excel))
}
if (form.value.import_to_db !== undefined) {
formData.append('import_to_db', String(form.value.import_to_db))
}
const partitionName = form.value.import_to_db ? form.value.partition_name : undefined
if (partitionName) {
formData.append('partition_name', partitionName)
}
const res = (await service.post('/xml/extract/process', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})) as any as ExtractResponse
if (res && res.success) {
totalCount.value = res.total_count || 0
importCount.value = res.import_count || 0
previewList.value = res.preview_data || []
// 构造下载 Excel 的链接
if (res.download_url) {
resultDownloadUrl.value = res.download_url
} else if (res.file_id) {
resultDownloadUrl.value = `${getBaseOrigin()}/translations/xml/extract/download/${res.file_id}/extracted_translations.xlsx`
}
loading.value = false
completed.value = true
window.$message.success('翻译对照对提取成功!')
} else {
throw new Error(res.message || '提取翻译对照失败')
}
} catch (err: any) {
loading.value = false
window.$message.error(`提取出错: ${err.message || err}`)
}
}
const downloadExcel = () => {
if (!resultDownloadUrl.value) return
const link = document.createElement('a')
link.href = resultDownloadUrl.value
link.download = `extracted_pairs_${Date.now()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
return {
showModal,
loading,
completed,
totalCount,
importCount,
resultDownloadUrl,
previewList,
form,
previewColumns,
open,
reset,
addTagPair,
removeTagPair,
startExtraction,
downloadExcel
}
}
<template>
<CommonModal
v-model="showModal"
title="提取 XML 翻译对照"
:width="700"
:loading="loading"
confirm-text="开始提取"
:show-confirm="!completed"
:show-cancel="!completed"
@confirm="startExtraction"
>
<div class="py-2">
<!-- 配置与对照标签对管理 -->
<div v-if="!loading && !completed" class="flex flex-col gap-4">
<n-form label-placement="left" label-width="120" size="medium">
<n-form-item label="生成 Excel 文件">
<n-space align="center">
<n-switch v-model:value="form.save_excel" />
<span class="text-xs text-color3">是否导出包含提取出来的中英文对照的 Excel</span>
</n-space>
</n-form-item>
<n-form-item label="导入至 Milvus">
<n-space align="center">
<n-switch v-model:value="form.import_to_db" />
<span class="text-xs text-color3">是否将提取得到的对照存入向量数据库中以备后用</span>
</n-space>
</n-form-item>
<n-form-item v-if="form.import_to_db" label="Milvus 分区名">
<n-input v-model:value="form.partition_name" placeholder="请输入数据库分区名称" />
</n-form-item>
</n-form>
<!-- 对照标签对配置 -->
<div class="border border-divider rounded-lg p-3">
<div class="flex justify-between items-center mb-3">
<span class="text-sm font-bold text-color1">配置提取标签对</span>
<CommonButton size="tiny" type="primary" secondary @click="addTagPair">添加标签对</CommonButton>
</div>
<div class="flex flex-col gap-2 max-h-[200px] overflow-y-auto pr-1">
<div
v-for="(pair, index) in form.tag_pairs"
:key="index"
class="flex items-center gap-2"
>
<n-input
v-model:value="pair.en_tag"
placeholder="源英文标签 (例: PARA)"
size="small"
class="flex-1"
/>
<n-icon class="text-color3">
<arrow-forward-outline />
</n-icon>
<n-input
v-model:value="pair.cn_tag"
placeholder="目标中文标签 (例: PARAC)"
size="small"
class="flex-1"
/>
<CommonButton
size="small"
type="error"
circle
quaternary
:disabled="form.tag_pairs.length <= 1"
@click="removeTagPair(index)"
>
<template #icon>
<trash-outline />
</template>
</CommonButton>
</div>
</div>
</div>
</div>
<!-- 运行中加载描述 -->
<div v-else-if="loading" class="flex flex-col items-center justify-center py-8 gap-4">
<div class="text-sm font-semibold text-color1">正在提取并处理翻译对照对,请稍候...</div>
</div>
<!-- 完成,展示提取到的预览数据和下载 -->
<div v-else-if="completed" class="flex flex-col gap-4">
<div class="flex items-center gap-3">
<n-icon size="32" class="text-success">
<checkmark-circle-outline />
</n-icon>
<div>
<div class="text-base font-bold text-color1">对照对提取处理成功!</div>
<div class="text-xs text-color3">
共提取中英双语对:<span class="text-primary font-bold text-sm">{{ totalCount }}</span>
<span v-if="form.import_to_db" class="ml-2">
,导入数据库:<span class="text-success font-bold text-sm">{{ importCount }}</span>
</span>
</div>
</div>
</div>
<!-- 提取数据预览列表 -->
<div class="border border-divider rounded-lg overflow-hidden">
<div class="bg-fill-3 px-3 py-2 text-xs font-bold text-color2 border-b border-divider">
数据预览 (展示前 10 条)
</div>
<n-data-table
:columns="previewColumns"
:data="previewList"
:max-height="250"
size="small"
:bordered="false"
/>
</div>
</div>
</div>
<template #footer v-if="completed">
<div class="flex justify-end gap-3 w-full">
<CommonButton @click="reset">重新提取</CommonButton>
<CommonButton v-if="form.save_excel" type="primary" @click="downloadExcel">下载 Excel 对照表</CommonButton>
<CommonButton @click="showModal = false">关闭</CommonButton>
</div>
</template>
</CommonModal>
</template>
<script setup lang="ts">
import { CheckmarkCircleOutline, TrashOutline, ArrowForwardOutline } from '@vicons/ionicons5'
import { useExtractTranslate } from './functionals'
const {
showModal,
loading,
completed,
totalCount,
importCount,
form,
previewColumns,
previewList,
open,
reset,
addTagPair,
removeTagPair,
startExtraction,
downloadExcel
} = useExtractTranslate()
defineExpose({
open
})
</script>
export interface SearchRequest {
query_text: string
search_type?: 'full_text' | 'text_match'
search_direction?: 'en_to_zh' | 'zh_to_en'
partition_names?: string[] | null
limit?: number
}
export interface SearchResult {
id: number | string
text: string
translation: string
score: number
fuzzy_score: number | null
partition_name: string | null
}
export interface SearchResponse {
success: boolean
results: SearchResult[]
count: number
message?: string
}
import { type SearchResult, type SearchResponse, type SearchRequest } from '../constants'
import { serviceManage } from '@/api'
import dayjs from 'dayjs'
export function useSearchTranslate() {
const showModal = ref(false)
const activeTab = ref('search')
const loading = ref(false)
const searched = ref(false)
const results = ref<SearchResult[]>([])
// 默认分区名从环境变量获取
const defaultPartition = import.meta.env.VITE_PARTITION_NAME
// 搜索表单
const form = ref({
query_text: '',
search_type: 'full_text' as 'full_text' | 'text_match',
search_direction: 'en_to_zh' as 'en_to_zh' | 'zh_to_en',
partition_name: defaultPartition,
limit: 5
})
// 新增表单
const addForm = ref({
text: '',
translation: '',
partition_name: defaultPartition
})
// 高级清理表单
const cleanForm = ref({
filter_expr: '',
partition_name: defaultPartition
})
// 图形化批量删除表单
const deleteForm = ref({
enableTextFilter: [] as string[],
textKey: 'text' as 'text' | 'translation',
textMatch: 'contains' as 'contains' | 'starts_with' | 'ends_with' | 'exact',
textValue: '',
enableTimeFilter: [] as string[],
timeKey: 'create_time' as 'create_time' | 'update_time',
startTime: null as string | null,
endTime: null as string | null
})
const searchTypeOptions = [
{ label: '全文搜索', value: 'full_text' },
{ label: '精确匹配', value: 'text_match' }
]
const directionOptions = [
{ label: '英译中 (EN -> ZH)', value: 'en_to_zh' },
{ label: '中译英 (ZH -> EN)', value: 'zh_to_en' }
]
const open = () => {
showModal.value = true
activeTab.value = 'search'
form.value.query_text = ''
results.value = []
searched.value = false
loading.value = false
// 重置新增表单
addForm.value = {
text: '',
translation: '',
partition_name: defaultPartition
}
// 重置清理表单
cleanForm.value = {
filter_expr: '',
partition_name: defaultPartition
}
// 重置图形化删除表单
deleteForm.value = {
enableTextFilter: [],
textKey: 'text',
textMatch: 'contains',
textValue: '',
enableTimeFilter: [],
timeKey: 'create_time',
startTime: null,
endTime: null
}
}
// 实时计算生成的过滤表达式
const currentGeneratedFilter = computed(() => {
const parts: string[] = []
if (deleteForm.value.enableTextFilter.includes('yes')) {
const key = deleteForm.value.textKey
const val = deleteForm.value.textValue.trim()
if (val) {
const safeVal = val.replace(/'/g, "\\'")
if (deleteForm.value.textMatch === 'contains') {
parts.push(`${key} like '%${safeVal}%'`)
} else if (deleteForm.value.textMatch === 'starts_with') {
parts.push(`${key} like '${safeVal}%'`)
} else if (deleteForm.value.textMatch === 'ends_with') {
parts.push(`${key} like '%${safeVal}'`)
} else if (deleteForm.value.textMatch === 'exact') {
parts.push(`${key} == '${safeVal}'`)
}
}
}
if (deleteForm.value.enableTimeFilter.includes('yes') && deleteForm.value.startTime && deleteForm.value.endTime) {
const key = deleteForm.value.timeKey
const startSec = Math.floor(dayjs(deleteForm.value.startTime).valueOf() / 1000)
const endSec = Math.floor(dayjs(deleteForm.value.endTime).valueOf() / 1000)
parts.push(`${key} >= ${startSec} and ${key} <= ${endSec}`)
}
return parts.join(' and ')
})
// 1. 搜索
const handleSearch = async () => {
if (!form.value.query_text.trim()) {
window.$message.warning('请输入搜索关键词')
return
}
loading.value = true
searched.value = true
try {
const params: SearchRequest = {
query_text: form.value.query_text.trim(),
search_type: form.value.search_type,
search_direction: form.value.search_direction,
limit: form.value.limit,
partition_names: form.value.partition_name.trim() ? [form.value.partition_name.trim()] : null
}
const res = (await serviceManage.postJson('/search', params)) as any as SearchResponse
if (res && res.success && res.results) {
results.value = res.results
} else {
results.value = []
window.$message.error(res.message || '查询失败')
}
} catch (e: any) {
window.$message.error(`搜索异常: ${e.message || e}`)
results.value = []
} finally {
loading.value = false
}
}
// 2. 添加单条翻译
const handleAdd = async () => {
if (!addForm.value.text.trim()) {
window.$message.warning('请输入英文原文')
return
}
if (!addForm.value.translation.trim()) {
window.$message.warning('请输入中文翻译')
return
}
if (!addForm.value.partition_name.trim()) {
window.$message.warning('请输入目标分区')
return
}
loading.value = true
try {
const params = {
text: addForm.value.text.trim(),
translation: addForm.value.translation.trim(),
partition_name: addForm.value.partition_name.trim()
}
const res = (await serviceManage.postJson('/data/insert_single', params)) as any
if (res && res.success) {
window.$message.success(res.message || '新增成功!')
// 清空表单
addForm.value.text = ''
addForm.value.translation = ''
} else {
window.$message.error(res.message || '新增失败')
}
} catch (e: any) {
window.$message.error(`新增异常: ${e.message || e}`)
} finally {
loading.value = false
}
}
// 3. 删除单条数据
const handleDeleteSingle = async (item: SearchResult) => {
if (!item.id) {
window.$message.error('无法删除无 ID 的数据项')
return
}
try {
await window.$dialog.warning({
title: '确认删除',
content: `确定要从数据库中删除此对照吗?\n【英】${item.text}\n【中】${item.translation}`
})
loading.value = true
try {
const params = {
ids: [item.id],
partition_name: item.partition_name || null
}
const res = (await serviceManage.deleteJson('/data/delete', params)) as any
if (res && res.success) {
window.$message.success('删除成功!')
results.value = results.value.filter((r) => r.id !== item.id)
} else {
window.$message.error(res.message || '删除失败')
}
} catch (e: any) {
window.$message.error(`删除异常: ${e.message || e}`)
} finally {
loading.value = false
}
} catch {
// 用户取消
}
}
// 4. 按过滤条件批量删除
const handleDeleteByFilter = async () => {
const expr = currentGeneratedFilter.value.trim()
if (!expr) {
window.$message.warning('请至少启用并配置一项有效的删除过滤条件')
return
}
try {
await window.$dialog.warning({
title: '确认条件删除',
content: `确定要从数据库中物理删除所有满足以下条件的数据吗?\n【条件】 ${expr}`
})
loading.value = true
try {
const params = {
filter_expr: expr,
partition_name: cleanForm.value.partition_name.trim() || null
}
const res = (await serviceManage.deleteJson('/data/delete', params)) as any
if (res && res.success) {
window.$message.success(res.message || `成功删除,影响数据行数: ${res.delete_count || 0}`)
// 重置图形化删除表单
deleteForm.value.enableTextFilter = []
deleteForm.value.textValue = ''
deleteForm.value.enableTimeFilter = []
deleteForm.value.startTime = null
deleteForm.value.endTime = null
} else {
window.$message.error(res.message || '删除失败')
}
} catch (e: any) {
window.$message.error(`删除异常: ${e.message || e}`)
} finally {
loading.value = false
}
} catch {
// 用户取消
}
}
const copyText = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
window.$message.success('已复制到剪贴板')
} catch (e) {
window.$message.error('复制失败')
}
}
return {
showModal,
activeTab,
loading,
searched,
results,
form,
addForm,
cleanForm,
deleteForm,
currentGeneratedFilter,
searchTypeOptions,
directionOptions,
open,
handleSearch,
handleAdd,
handleDeleteSingle,
handleDeleteByFilter,
copyText
}
}
<template>
<CommonModal v-model="showModal" title="双语翻译云端数据库 (Milvus)" :width="800" :show-confirm="false" cancel-text="关闭">
<div class="py-1">
<n-tabs type="segment" v-model:value="activeTab" class="mb-4">
<!-- 1. 翻译检索标签页 -->
<n-tab-pane name="search" tab="检索双语翻译">
<div class="flex flex-col gap-4">
<!-- 搜索框及选项区 -->
<div class="flex items-center gap-2">
<n-input
v-model:value="form.query_text"
placeholder="输入要查询的技术英语文本或中文词汇..."
size="large"
class="flex-1"
clearable
@keyup.enter="handleSearch"
>
<template #prefix>
<n-icon><search-outline /></n-icon>
</template>
</n-input>
<CommonButton type="primary" size="large" :loading="loading" @click="handleSearch">搜索</CommonButton>
</div>
<!-- 搜索高级设置 -->
<div class="grid grid-cols-3 gap-4 bg-fill-2 p-3 rounded-lg border border-divider text-xs">
<div class="flex flex-col gap-1">
<span class="text-color3 font-medium">检索模式:</span>
<CommonSelect v-model:value="form.search_type" :options="searchTypeOptions" size="small" />
</div>
<div class="flex flex-col gap-1">
<span class="text-color3 font-medium">翻译方向:</span>
<CommonSelect v-model:value="form.search_direction" :options="directionOptions" size="small" />
</div>
<div class="flex flex-col gap-1">
<span class="text-color3 font-medium">返回结果数:</span>
<CommonInputNumber v-model:value="form.limit" :min="1" :max="25" size="small" :strict="true" />
</div>
</div>
<!-- 搜索结果列表 -->
<div class="min-h-[300px] flex flex-col">
<div v-if="loading" class="flex-1 flex flex-col items-center justify-center gap-3">
<n-spin size="large" />
<span class="text-xs text-color3">正在云端检索双语库...</span>
</div>
<div
v-else-if="searched && results.length === 0"
class="flex-1 flex flex-col items-center justify-center text-color3 py-12"
>
<n-empty description="未检索到匹配的翻译对照" />
</div>
<div v-else-if="!searched" class="flex-1 flex flex-col items-center justify-center text-color3 py-16">
<n-icon size="40" class="text-color3 opacity-50 mb-2"><search-outline /></n-icon>
<span class="text-xs">支持通过 Milvus 向量相似度搜索查找专业适航术语对照</span>
</div>
<div v-else class="flex flex-col gap-3 max-h-[400px] overflow-y-auto pr-1">
<div
v-for="(item, index) in results"
:key="index"
class="border border-divider rounded-lg p-3 hover:border-primary hover:shadow-sm transition-all duration-200 bg-fill-1 flex flex-col gap-2"
>
<div class="flex justify-between items-center gap-2">
<div class="flex items-center gap-2">
<span class="text-xs text-primary font-bold">匹配 #{{ index + 1 }}</span>
<CommonTag size="tiny" type="info" v-if="item.score">分值: {{ item.score.toFixed(3) }}</CommonTag>
<CommonTag size="tiny" type="warning" v-if="item.fuzzy_score">
相似度: {{ item.fuzzy_score.toFixed(1) }}%
</CommonTag>
<CommonTag size="tiny" v-if="item.partition_name">分区: {{ item.partition_name }}</CommonTag>
</div>
<!-- 单条删除按钮 -->
<CommonButton
size="tiny"
type="error"
quaternary
circle
@click="handleDeleteSingle(item)"
title="从数据库删除此条"
>
<template #icon>
<n-icon><trash-outline /></n-icon>
</template>
</CommonButton>
</div>
<div class="grid grid-cols-2 gap-4">
<!-- 英文 -->
<div class="bg-fill-2 p-2.5 rounded border border-divider relative group">
<div class="text-[10px] text-color3 mb-1 font-medium">英文原文</div>
<div class="text-xs font-semibold text-color1 break-words select-text pr-8">{{ item.text }}</div>
<CommonButton
size="tiny"
quaternary
class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
@click="copyText(item.text)"
>
<template #icon>
<n-icon><copy-outline /></n-icon>
</template>
复制
</CommonButton>
</div>
<!-- 中文 -->
<div class="bg-fill-2 p-2.5 rounded border border-divider relative group">
<div class="text-[10px] text-color3 mb-1 font-medium">中文翻译</div>
<div class="text-xs font-semibold text-color1 break-words select-text pr-8">{{ item.translation }}</div>
<CommonButton
size="tiny"
quaternary
class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
@click="copyText(item.translation)"
>
<template #icon>
<n-icon><copy-outline /></n-icon>
</template>
复制
</CommonButton>
</div>
</div>
</div>
</div>
</div>
<!-- 检索结果提示 -->
<div v-if="results.length > 0" class="text-xs text-color3 text-right">共检索到 {{ results.length }} 条结果</div>
</div>
</n-tab-pane>
<!-- 2. 新增翻译对照标签页 -->
<n-tab-pane name="add" tab="新增翻译对照">
<div class="bg-fill-1 border border-divider rounded-lg p-5 flex flex-col gap-4">
<div class="text-sm font-semibold border-b border-divider pb-2 flex items-center gap-1.5">
<n-icon class="text-primary"><add-circle-outline /></n-icon>
添加单条双语翻译至云端数据库
</div>
<n-form label-placement="left" label-width="80" size="medium">
<n-form-item label="英文原文">
<n-input
v-model:value="addForm.text"
type="textarea"
:rows="3"
placeholder="请输入需要保存的英文术语或原文段落..."
/>
</n-form-item>
<n-form-item label="中文翻译">
<n-input
v-model:value="addForm.translation"
type="textarea"
:rows="3"
placeholder="请输入对应的中文标准翻译..."
/>
</n-form-item>
<div class="flex justify-end mt-2">
<CommonButton type="primary" size="medium" :loading="loading" @click="handleAdd">
<template #icon>
<n-icon><add-circle-outline /></n-icon>
</template>
写入数据库
</CommonButton>
</div>
</n-form>
</div>
</n-tab-pane>
<!-- 3. 批量删除标签页 -->
<n-tab-pane name="clean" tab="批量删除">
<div class="flex flex-col gap-4">
<!-- 警示卡片:说明删除操作 -->
<div
class="bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-900 rounded-lg p-4 text-xs text-amber-800 dark:text-amber-300 flex items-start gap-2"
>
<n-icon size="18" class="mt-0.5"><alert-circle-outline /></n-icon>
<div class="flex-1 flex flex-col gap-1">
<div class="font-bold text-sm">重要安全说明</div>
<div>1. 所有删除操作均为物理删除,执行后无法撤销,请务必核实后操作!</div>
<div>2. 请通过下方图形化选项配置过滤条件,系统将自动生成对应的数据库安全删除指令。</div>
</div>
</div>
<!-- 图形化配置面板 -->
<div class="bg-fill-1 border border-divider rounded-lg p-5 flex flex-col gap-4">
<div class="text-sm font-semibold border-b border-divider pb-2 flex items-center gap-1.5">
<n-icon class="text-warning"><alert-circle-outline /></n-icon>
图形化批量删除过滤配置
</div>
<div class="flex flex-col gap-4">
<!-- 选项一:按内容删除 -->
<div class="border border-divider rounded-lg p-4 bg-fill-2 flex flex-col gap-3">
<div class="flex items-center gap-2">
<CommonCheckbox
v-model:value="deleteForm.enableTextFilter"
:options="[{ label: '按翻译文本内容过滤', value: 'yes' }]"
/>
</div>
<div v-if="deleteForm.enableTextFilter.includes('yes')" class="grid grid-cols-3 gap-3 items-center">
<div class="flex flex-col gap-1">
<span class="text-xs text-color3">过滤字段</span>
<CommonSelect
v-model:value="deleteForm.textKey"
:options="[
{ label: '英文原文 (text)', value: 'text' },
{ label: '中文翻译 (translation)', value: 'translation' }
]"
size="small"
/>
</div>
<div class="flex flex-col gap-1">
<span class="text-xs text-color3">匹配方式</span>
<CommonSelect
v-model:value="deleteForm.textMatch"
:options="[
{ label: '包含', value: 'contains' },
{ label: '开头为', value: 'starts_with' },
{ label: '结尾为', value: 'ends_with' },
{ label: '精确等于', value: 'exact' }
]"
size="small"
/>
</div>
<div class="flex flex-col gap-1">
<span class="text-xs text-color3">关键词</span>
<n-input v-model:value="deleteForm.textValue" placeholder="输入文本关键词..." size="small" />
</div>
</div>
</div>
<!-- 选项二:按时间段删除 -->
<div class="border border-divider rounded-lg p-4 bg-fill-2 flex flex-col gap-3">
<div class="flex items-center gap-2">
<CommonCheckbox
v-model:value="deleteForm.enableTimeFilter"
:options="[{ label: '按数据创建/更新时间范围过滤', value: 'yes' }]"
/>
</div>
<div v-if="deleteForm.enableTimeFilter.includes('yes')" class="grid grid-cols-2 gap-3 items-center">
<div class="flex flex-col gap-1">
<span class="text-xs text-color3">时间字段</span>
<CommonSelect
v-model:value="deleteForm.timeKey"
:options="[
{ label: '创建时间 (create_time)', value: 'create_time' },
{ label: '更新时间 (update_time)', value: 'update_time' }
]"
size="small"
/>
</div>
<div class="flex flex-col gap-1">
<span class="text-xs text-color3">选择时间段</span>
<CommonDatePicker v-model:start="deleteForm.startTime" v-model:end="deleteForm.endTime" type="datetimerange" size="small" />
</div>
</div>
</div>
<!-- 条件实时预览 -->
<div class="bg-fill-3 p-3 rounded border border-divider text-xs flex flex-col gap-1">
<span class="text-color3 font-medium">当前生成的数据库删除表达式预览:</span>
<code class="text-primary break-all font-mono">
{{ currentGeneratedFilter || '(请勾选并配置上方条件)' }}
</code>
</div>
</div>
<div class="flex justify-end mt-2">
<CommonButton type="warning" size="medium" :loading="loading" @click="handleDeleteByFilter">执行批量删除</CommonButton>
</div>
</div>
</div>
</n-tab-pane>
</n-tabs>
</div>
</CommonModal>
</template>
<script setup lang="ts">
import { SearchOutline, AddCircleOutline, AlertCircleOutline, CopyOutline, TrashOutline } from '@vicons/ionicons5'
import { useSearchTranslate } from './functionals'
const {
showModal,
activeTab,
loading,
searched,
results,
form,
addForm,
cleanForm,
deleteForm,
currentGeneratedFilter,
searchTypeOptions,
directionOptions,
open,
handleSearch,
handleAdd,
handleDeleteSingle,
handleDeleteByFilter,
copyText
} = useSearchTranslate()
defineExpose({
open
})
</script>
......@@ -7,7 +7,7 @@ export const TOOLBAR_TITLE = 'XML 编辑工具栏'
export const GREEN_BUTTONS: any[] = [
// { label: '插入图片', tag: 'GRAPHIC', icon: ImageOutline },
// { label: '插入表格', tag: 'TABLE', icon: GridOutline },
{ label: '插入表格', tag: 'TABLE', icon: GridOutline }
// { label: '插入模板', tag: 'PRETOPIC', icon: DocumentTextOutline },
// { label: '插入签字点', tag: 'SIGNOFF', icon: CreateOutline }
]
import { useEditorStore } from '@/store/editor'
import { useEditorStore, createTableStructure } from '@/store/editor'
import { useAppStore } from '@/store/app/index'
import { canAddChild } from '@/utils/dtdManager'
/**
* EditorToolbar 组件级业务逻辑 Hook
......@@ -11,17 +12,62 @@ export function useEditorToolbar(emit: any) {
const insertBelow = ref(true)
const fileInputRef = ref<HTMLInputElement | null>(null)
const isUploading = ref(false)
const createTableModalRef = ref<any>(null)
const canUndo = computed(() => editorStore.undoStack.length > 0)
const canRedo = computed(() => editorStore.redoStack.length > 0)
const handleInsert = (tag: string) => {
editorStore.insertNode(tag, insertBelow.value)
const selected = editorStore.selectedNode
if (!selected) {
window.$message.warning('请先在树中选择一个目标节点!')
return
}
let parentTag = ''
let parentNode: XmlNode | null = null
if (insertBelow.value) {
parentNode = editorStore.selectedNodeParent
if (!parentNode) {
window.$message.warning('无法在根节点下方插入兄弟节点')
return
}
parentTag = parentNode.tagName
} else {
parentNode = selected
parentTag = selected.tagName
}
const existingCount = parentNode.children.filter((c) => c.tagName === tag).length
if (!canAddChild(parentTag, tag, existingCount)) {
window.$message.warning(`DTD 校验失败: 节点 <${parentTag}> 无法接受子元素 <${tag}>`)
return
}
if (tag === 'TABLE') {
createTableModalRef.value?.open()
} else {
editorStore.insertNode(tag, insertBelow.value)
}
}
const handleCreateTableConfirm = (rows: number, cols: number, cellChildTags: string[]) => {
const tableNode = createTableStructure(rows, cols, cellChildTags)
editorStore.insertNode('TABLE', insertBelow.value, tableNode)
}
const batchTranslateModalRef = ref<any>(null)
const extractTranslateModalRef = ref<any>(null)
const searchTranslateModalRef = ref<any>(null)
const handleTranslate = (type: 'batch' | 'extract' | 'search') => {
const labelMap = { batch: '批量翻译', extract: '提取翻译', search: '搜索翻译' }
window.$message.info(`已触发 ${labelMap[type]} 功能,自动匹配双语对照。`)
if (type === 'batch') {
batchTranslateModalRef.value?.open()
} else if (type === 'extract') {
extractTranslateModalRef.value?.open()
} else if (type === 'search') {
searchTranslateModalRef.value?.open()
}
}
const triggerUpload = () => {
......@@ -68,6 +114,11 @@ export function useEditorToolbar(emit: any) {
handleInsert,
handleTranslate,
triggerUpload,
createTableModalRef,
handleCreateTableConfirm,
batchTranslateModalRef,
extractTranslateModalRef,
searchTranslateModalRef,
handleFileUpload: () => {} // 保留空函数以防组件 template 尚未完全更新时报错
}
}
......@@ -50,11 +50,29 @@
<div class="flex items-center gap-1.5 flex-shrink-0 ml-auto">
<!-- 翻译辅助组 -->
<div class="flex items-center gap-1 bg-fill-3 px-1 py-0.5 rounded-md border border-divider flex-shrink-0">
<CommonButton size="small" quaternary class="util-btn" @click="handleTranslate('batch')">
<CommonButton
size="small"
quaternary
class="util-btn"
:type="batchTranslateModalRef?.completed ? 'success' : (batchTranslateModalRef?.loading ? 'primary' : 'default')"
@click="handleTranslate('batch')"
>
<template #icon>
<n-icon><language-outline /></n-icon>
<n-icon>
<sync-outline v-if="batchTranslateModalRef?.loading" class="animate-spin text-primary" />
<checkmark-circle-outline v-else-if="batchTranslateModalRef?.completed" class="text-success" />
<language-outline v-else />
</n-icon>
</template>
批量翻译
<span v-if="batchTranslateModalRef?.loading">
批量翻译 ({{ batchTranslateModalRef.progress }}%)
</span>
<span v-else-if="batchTranslateModalRef?.completed">
批量翻译 (已完成)
</span>
<span v-else>
批量翻译
</span>
</CommonButton>
<CommonButton size="small" quaternary class="util-btn" @click="handleTranslate('extract')">
<template #icon>
......@@ -145,6 +163,18 @@
<!-- 插入 XML 片段弹窗 -->
<InsertFragmentModal ref="insertFragmentModalRef" />
<!-- 插入表格弹窗 -->
<CreateTableModal ref="createTableModalRef" @confirm="handleCreateTableConfirm" />
<!-- 批量翻译弹窗 -->
<BatchTranslateModal ref="batchTranslateModalRef" />
<!-- 提取翻译对照弹窗 -->
<ExtractTranslateModal ref="extractTranslateModalRef" />
<!-- 搜索翻译数据库弹窗 -->
<SearchTranslateModal ref="searchTranslateModalRef" />
</div>
</template>
......@@ -160,20 +190,40 @@ import {
ArrowRedoOutline,
SunnyOutline,
MoonOutline,
CodeWorkingOutline
CodeWorkingOutline,
SyncOutline,
CheckmarkCircleOutline
} from '@vicons/ionicons5'
import { useEditorToolbar } from './functionals'
import { GREEN_BUTTONS } from './constants'
import SettingsDrawer from '@/layouts/components/SettingsDrawer.vue'
import InsertFragmentModal from './components/InsertFragmentModal/index.vue'
import CreateTableModal from './components/CreateTableModal/index.vue'
import BatchTranslateModal from './components/BatchTranslateModal/index.vue'
import ExtractTranslateModal from './components/ExtractTranslateModal/index.vue'
import SearchTranslateModal from './components/SearchTranslateModal/index.vue'
const emit = defineEmits(['save', 'validate', 'export', 'preview'])
const { editorStore, appStore, insertBelow, fileInputRef, isUploading, canUndo, canRedo, handleInsert, handleTranslate, triggerUpload } =
useEditorToolbar(emit)
const {
editorStore,
appStore,
insertBelow,
fileInputRef,
isUploading,
canUndo,
canRedo,
handleInsert,
handleTranslate,
triggerUpload,
createTableModalRef,
handleCreateTableConfirm,
batchTranslateModalRef,
extractTranslateModalRef,
searchTranslateModalRef
} = useEditorToolbar(emit)
const insertFragmentModalRef = ref<any>(null)
</script>
const insertFragmentModalRef = ref<any>(null)</script>
<style scoped>
/* 消除工具栏容器本身的 focus outline */
......
import type { FormInst } from 'naive-ui'
import { getElementAttributes, createDefaultAttributes, isTextOnlyElement, isMixedContentElement } from '@/utils/dtdManager'
import { nextTick } from 'vue'
import { useEditorStore } from '@/store/editor'
import { useAppStore } from '@/store/app'
import type { XmlNode, DtdAttribute } from '@/types/xmlNode'
import type { AttrDef } from '../../../constants'
import { addNodeVisible, addNodeMode, addNodeTargetId, addNodeAllowedTags } from '../../../functionals'
......@@ -336,10 +338,22 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) {
}
}
const appStore = useAppStore()
if (!appStore.autoExpandOnInsert) {
store.skipExpandOnSelect = true
}
store.rebuildNodeMap()
store.setSelectedNodeId(newId)
store.handleNodeInsertion(newNode)
window.$message?.success(`成功插入节点 <${form.tagName}>`)
addNodeVisible.value = false
if (!appStore.autoExpandOnInsert) {
nextTick(() => {
store.skipExpandOnSelect = false
})
}
}
}
} finally {
......
......@@ -50,3 +50,23 @@ export interface SafeNodeItem {
parentPath: string
checked: boolean
}
// 翻译请求接口
export interface TranslationRequest {
text: string
partition_names?: string[] | null
stream?: boolean
fuzzy_score_threshold?: number | null
search_direction?: 'en_to_zh' | 'zh_to_en'
save_to_database?: boolean
database_partition_name?: string | null
}
// 翻译响应接口
export interface TranslationResponse {
success: boolean
translation: string
is_direct_match: boolean
fuzzy_score: number | null
reference_count: number
}
......@@ -13,13 +13,15 @@ import {
BuildOutline,
FolderOpenOutline,
DocumentTextOutline,
CodeWorkingOutline
CodeWorkingOutline,
LanguageOutline
} from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor'
import { useAppStore } from '@/store/app'
import type { XmlNode } from '@/types/xmlNode'
import { serializeTreeToXml } from '@/utils/xmlParser'
import { isMixedContentElement, isTextOnlyElement, sortChildrenByDtd } from '@/utils/dtdManager'
import type { CheckRuleData, InsertMode, FlatNode } from '../constants'
import type { CheckRuleData, InsertMode, FlatNode, TranslationResponse } from '../constants'
import { DOCUMENT_LIKE_TAGS } from '../constants'
import { STRUCTURAL_TAGS, WARNING_LIKE_TAGS, NOTE_AND_REF_TAGS, COLORED_TAGS } from '@/configs/xmlTags'
......@@ -52,6 +54,11 @@ export const addNodeAllowedTags = ref<string[]>([])
export const copyNodeCache = ref<XmlNode | null>(null)
// ══════════════════════════════════════════════════════════
// 全局共享状态:智能翻译中的节点 ID
// ══════════════════════════════════════════════════════════
export const translatingNodeId = ref<string | null>(null)
// ══════════════════════════════════════════════════════════
// Helper:渲染图标
// ══════════════════════════════════════════════════════════
const icon = (component: any) => () => h(NIcon, null, { default: () => h(component) })
......@@ -66,6 +73,7 @@ export function useNodeTree(
onOpenBatchDeleteConfirm?: (selectedNodeIds: string[]) => void
) {
const editorStore = useEditorStore()
const appStore = useAppStore()
const pattern = ref('')
const isTreeSelecting = ref(false)
......@@ -168,6 +176,10 @@ export function useNodeTree(
if (textItems.length > 1) {
return '' // 多个文本节点时,不展示内容区
}
// 没有直接文本,内容全部来自子元素 — 不展示父节点 subtitle
if (textItems.length === 0) {
return ''
}
return n.mixedContent
.map((item) => {
if (item.type === 'text') {
......@@ -180,8 +192,9 @@ export function useNodeTree(
})
.join('')
}
// 有子元素但无直接文本 — 文本归属于子节点,父节点不展示
if (n.children && n.children.length > 0) {
return n.children.map(getFullText).join('')
return ''
}
return (n.textContent || '').trim()
}
......@@ -428,7 +441,7 @@ export function useNodeTree(
}
// 树展开与定位同步辅助函数
const syncTreeSelection = (newId: string | null) => {
const syncTreeSelection = (newId: string | null, shouldExpand = true) => {
if (!newId) return
let isVirtual = false
......@@ -445,7 +458,7 @@ export function useNodeTree(
path.unshift(curr.node)
curr = curr.parent ? editorStore.nodeMap.get(curr.parent.id) : undefined
}
if (path.length > 0) {
if (path.length > 0 && shouldExpand) {
let changed = false
// 若为虚拟文本子节点,则父节点本身(即 path 最后一项)也需要被自动加入展开集合中
const limit = isVirtual ? path.length : path.length - 1
......@@ -481,7 +494,12 @@ export function useNodeTree(
}
// 监听选中的节点,进行树组件的自动展开与滚动定位
watch(() => editorStore.selectedNodeId, syncTreeSelection)
watch(
() => editorStore.selectedNodeId,
(newId) => {
syncTreeSelection(newId, !editorStore.skipExpandOnSelect)
}
)
// 监听回退/重做以同步树的展开与定位
watch(
......@@ -496,11 +514,154 @@ export function useNodeTree(
() => editorStore.xmlTree,
() => {
nextTick(() => {
syncTreeSelection(editorStore.selectedNodeId)
syncTreeSelection(editorStore.selectedNodeId, !editorStore.skipExpandOnSelect)
})
}
)
const getEnglishSourceNode = (node: XmlNode, parent: XmlNode | null): XmlNode | null => {
if (!parent || !node.tagName.endsWith('C')) return null
const enTag = node.tagName.slice(0, -1)
if (!getElementRule(enTag)) return null
const idx = parent.children.findIndex((c) => c.id === node.id)
if (idx === -1) return null
for (let i = idx + 1; i < parent.children.length; i++) {
const sibling = parent.children[i]
if (sibling.tagName === node.tagName) break
if (sibling.tagName === enTag) {
return sibling
}
}
return null
}
/** 检查节点是否包含可翻译文本(直接或子孙内) */
const hasTranslatableText = (n: XmlNode): boolean => {
if ((n.textContent || '').trim()) return true
return n.children.some(hasTranslatableText)
}
/**
* 递归翻译英文源节点到对应中文目标节点,分三种情形:
* 1. 混合内容节点(mixedContent 中含 text + element 交替)
* — 按 mixedContent 顺序逐段翻译文本,element 项按 tagName+顺序匹配目标子节点递归处理
* — 最终将组装好的新 mixedContent 写回 targetNode
* 2. 叶节点(无子元素,有直接 textContent)
* — 单次接口翻译后直接覆写 targetNode.textContent
* 3. 纯容器节点(只有子元素,无直接文本)
* — 按 tagName 顺序匹配子节点对递归处理
* 返回是否至少有一处翻译成功
*/
const translateNodePairs = async (sourceNode: XmlNode, targetNode: XmlNode): Promise<boolean> => {
// ── 情形 1:混合内容节点(PARA 等含 text + inline element)──
if (sourceNode.mixedContent && sourceNode.mixedContent.length > 0 && isMixedContentElement(sourceNode.tagName)) {
// 将目标子节点按 tagName 建立顺序索引,用于匹配 element 项
const targetByTag: Record<string, XmlNode[]> = {}
for (const child of targetNode.children) {
if (!targetByTag[child.tagName]) targetByTag[child.tagName] = []
targetByTag[child.tagName].push(child)
}
const tagUsedCount: Record<string, number> = {}
const newMixedContent: import('@/types/xmlNode').MixedContentItem[] = []
let anySuccess = false
for (const item of sourceNode.mixedContent) {
if (item.type === 'text') {
const rawText = (item.text || '').trim()
if (rawText) {
const res = (await service.postJson('/translate', {
text: rawText,
search_direction: 'en_to_zh'
})) as any as TranslationResponse
if (res?.success && res?.translation) {
newMixedContent.push({ type: 'text', text: res.translation })
anySuccess = true
} else {
newMixedContent.push(item) // 保留原文
}
} else {
newMixedContent.push(item)
}
} else if (item.type === 'element' && item.nodeId) {
const srcChild = sourceNode.children.find((c) => c.id === item.nodeId)
if (srcChild) {
const tag = srcChild.tagName
const usedIdx = tagUsedCount[tag] || 0
tagUsedCount[tag] = usedIdx + 1
const tgtChild = (targetByTag[tag] || [])[usedIdx]
if (tgtChild) {
const ok = await translateNodePairs(srcChild, tgtChild)
if (ok) anySuccess = true
newMixedContent.push({ type: 'element', nodeId: tgtChild.id })
} else {
newMixedContent.push(item)
}
} else {
newMixedContent.push(item)
}
}
}
if (anySuccess) {
targetNode.mixedContent = newMixedContent
targetNode.textContent = newMixedContent
.filter((i) => i.type === 'text')
.map((i) => i.text || '')
.join('')
}
return anySuccess
}
// ── 情形 2:叶节点(无子元素,有直接文本)──
const sourceText = (sourceNode.textContent || '').trim()
if (sourceText && sourceNode.children.length === 0) {
const res = (await service.postJson('/translate', {
text: sourceText,
search_direction: 'en_to_zh'
})) as any as TranslationResponse
if (res?.success && res?.translation) {
targetNode.textContent = res.translation
if (isMixedContentElement(targetNode.tagName)) {
targetNode.mixedContent = [{ type: 'text', text: res.translation }]
}
return true
}
return false
}
// ── 情形 3:纯容器节点(只有子元素,无直接文本)──
if (sourceNode.children.length > 0) {
const sourceByTag: Record<string, XmlNode[]> = {}
for (const child of sourceNode.children) {
if (!sourceByTag[child.tagName]) sourceByTag[child.tagName] = []
sourceByTag[child.tagName].push(child)
}
const targetByTag: Record<string, XmlNode[]> = {}
for (const child of targetNode.children) {
if (!targetByTag[child.tagName]) targetByTag[child.tagName] = []
targetByTag[child.tagName].push(child)
}
let anySuccess = false
for (const [, srcChildren] of Object.entries(sourceByTag)) {
const tgtChildren = targetByTag[srcChildren[0].tagName] || []
for (let i = 0; i < srcChildren.length; i++) {
const tgtChild = tgtChildren[i]
if (tgtChild) {
const ok = await translateNodePairs(srcChildren[i], tgtChild)
if (ok) anySuccess = true
}
}
}
return anySuccess
}
return false
}
// ── 生成右键下拉菜单选项 ──────────────────────────────
const getDropdownOptions = (nodeId: string): DropdownOption[] => {
const tree = editorStore.xmlTree
......@@ -559,6 +720,16 @@ export function useNodeTree(
icon: icon(CreateOutline)
})
// 智能翻译
const enSourceNode = getEnglishSourceNode(node, parent)
if (enSourceNode && !isVirtual && hasTranslatableText(enSourceNode)) {
options.push({
label: '智能翻译',
key: 'translateNode',
icon: icon(LanguageOutline)
})
}
// 复制节点
options.push({
label: '复制节点',
......@@ -586,7 +757,7 @@ export function useNodeTree(
})
}
// 粘贴XML片段
// 粘贴XML
const pasteFragmentChildren: DropdownOption[] = isVirtual
? [
{ label: '粘贴到上方', key: 'pasteFragmentAbove', icon: icon(ClipboardOutline), disabled: !parent },
......@@ -598,7 +769,7 @@ export function useNodeTree(
{ label: '粘贴到内部', key: 'pasteFragmentInside', icon: icon(ClipboardOutline) }
]
options.push({
label: '粘贴XML片段',
label: '粘贴XML',
key: 'pasteXmlFragment',
icon: icon(ClipboardOutline),
children: pasteFragmentChildren
......@@ -744,6 +915,36 @@ export function useNodeTree(
break
}
// ── 智能翻译节点 ──
case 'translateNode': {
const enSourceNode = getEnglishSourceNode(node, parent)
if (!enSourceNode) break
if (!hasTranslatableText(enSourceNode)) {
window.$message?.warning('英文原文内容为空,无需翻译')
break
}
try {
translatingNodeId.value = node.id
editorStore.saveSnapshot()
const ok = await translateNodePairs(enSourceNode, node)
if (ok) {
window.$message?.success('翻译已成功填入')
editorStore.rebuildNodeMap()
} else {
window.$message?.error('智能翻译失败:接口未返回有效数据')
}
} catch (err: any) {
console.error(err)
} finally {
translatingNodeId.value = null
}
break
}
// ── 复制节点 ──
case 'copyNode': {
copyNodeCache.value = JSON.parse(JSON.stringify(node)) // 深拷贝
......@@ -757,6 +958,11 @@ export function useNodeTree(
const cloned = deepCloneWithNewIds(copyNodeCache.value, parent.id)
editorStore.saveSnapshot()
const appStore = useAppStore()
if (!appStore.autoExpandOnInsert) {
editorStore.skipExpandOnSelect = true
}
if (cloned.tagName === '#text') {
if (isVirtual) {
const textIdx = parseInt(nodeId.split('-txt-')[1], 10)
......@@ -793,6 +999,13 @@ export function useNodeTree(
}
} else {
editorStore.setSelectedNodeId(cloned.id)
editorStore.handleNodeInsertion(cloned)
}
if (!appStore.autoExpandOnInsert) {
nextTick(() => {
editorStore.skipExpandOnSelect = false
})
}
window.$message?.success('粘贴成功')
break
......@@ -804,6 +1017,11 @@ export function useNodeTree(
const cloned = deepCloneWithNewIds(copyNodeCache.value, parent.id)
editorStore.saveSnapshot()
const appStore = useAppStore()
if (!appStore.autoExpandOnInsert) {
editorStore.skipExpandOnSelect = true
}
if (cloned.tagName === '#text') {
if (isVirtual) {
const textIdx = parseInt(nodeId.split('-txt-')[1], 10)
......@@ -840,6 +1058,13 @@ export function useNodeTree(
}
} else {
editorStore.setSelectedNodeId(cloned.id)
editorStore.handleNodeInsertion(cloned)
}
if (!appStore.autoExpandOnInsert) {
nextTick(() => {
editorStore.skipExpandOnSelect = false
})
}
window.$message?.success('粘贴成功')
break
......@@ -850,14 +1075,27 @@ export function useNodeTree(
if (!copyNodeCache.value) break
const cloned = deepCloneWithNewIds(copyNodeCache.value, nodeId)
editorStore.saveSnapshot()
const appStore = useAppStore()
if (!appStore.autoExpandOnInsert) {
editorStore.skipExpandOnSelect = true
}
node.children.push(cloned)
editorStore.rebuildNodeMap()
editorStore.setSelectedNodeId(cloned.id)
editorStore.handleNodeInsertion(cloned)
if (!appStore.autoExpandOnInsert) {
nextTick(() => {
editorStore.skipExpandOnSelect = false
})
}
window.$message?.success('粘贴成功')
break
}
// ── 粘贴XML片段到上方 ──
// ── 粘贴XML到上方 ──
case 'pasteFragmentAbove': {
if (onOpenInsertFragment) {
onOpenInsertFragment('above', nodeId)
......@@ -865,7 +1103,7 @@ export function useNodeTree(
break
}
// ── 粘贴XML片段到下方 ──
// ── 粘贴XML到下方 ──
case 'pasteFragmentBelow': {
if (onOpenInsertFragment) {
onOpenInsertFragment('below', nodeId)
......@@ -873,7 +1111,7 @@ export function useNodeTree(
break
}
// ── 粘贴XML片段到内部 ──
// ── 粘贴XML到内部 ──
case 'pasteFragmentInside': {
if (onOpenInsertFragment) {
onOpenInsertFragment('inside', nodeId)
......@@ -1022,7 +1260,7 @@ export function useNodeTree(
await handleDropdownAction(key, contextNodeId.value)
// 如果添加了节点,确保父级节点展开状态
if (key.startsWith('add-child-')) {
if (key.startsWith('add-child-') && appStore.autoExpandOnInsert) {
if (!expandedKeys.value.has(contextNodeId.value)) {
expandedKeys.value.add(contextNodeId.value)
expandedKeys.value = new Set(expandedKeys.value)
......
......@@ -81,6 +81,25 @@
@click="handleSelect(item.id)"
@contextmenu.prevent="(e) => handleContextMenu(e, item)"
>
<!-- 局部翻译加载状态 -->
<Transition name="translate-loading">
<div
v-if="translatingNodeId === item.id"
class="translate-loading-mask"
>
<div class="translate-loading-inner">
<n-icon size="13" class="translate-loading-icon">
<SyncOutline />
</n-icon>
<span class="translate-loading-text">
正在智能翻译
<span class="translate-dots">
<span>.</span><span>.</span><span>.</span>
</span>
</span>
</div>
</div>
</Transition>
<!-- Checkbox (批量管理模式) -->
<n-checkbox
v-if="isBatchMode"
......@@ -178,9 +197,9 @@
</template>
<script setup lang="ts">
import { SearchOutline, ListOutline, TrashOutline, CloseOutline } from '@vicons/ionicons5'
import { SearchOutline, ListOutline, TrashOutline, CloseOutline, SyncOutline } from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor'
import { useNodeTree, checkRuleVisible, viewXmlVisible, addNodeVisible } from './functionals'
import { useNodeTree, checkRuleVisible, viewXmlVisible, addNodeVisible, translatingNodeId } from './functionals'
import CheckRuleModal from './components/CheckRuleModal/index.vue'
import AddNodeModal from './components/AddNodeModal/index.vue'
import ViewXmlModal from './components/ViewXmlModal/index.vue'
......@@ -325,4 +344,81 @@ const batchDeleteConfirmModalRef = ref<any>(null)
color: var(--primary-color) !important;
opacity: 0.8;
}
/* ── 智能翻译 局部加载遮罩 ── */
.translate-loading-mask {
position: absolute;
inset: 0;
z-index: 20;
pointer-events: none;
border-radius: 4px;
display: flex;
align-items: center;
overflow: hidden;
/* 从左向右渐变的主色条纹 */
background: linear-gradient(
90deg,
var(--primary-color) 0%,
color-mix(in srgb, var(--primary-color) 85%, transparent) 60%,
color-mix(in srgb, var(--primary-color) 50%, transparent) 100%
);
}
.translate-loading-inner {
display: flex;
align-items: center;
gap: 5px;
padding: 0 10px;
width: 100%;
}
.translate-loading-icon {
color: #fff;
flex-shrink: 0;
animation: translate-spin 0.8s linear infinite;
}
.translate-loading-text {
font-size: 12px;
font-weight: 600;
color: #fff;
letter-spacing: 0.02em;
white-space: nowrap;
display: flex;
align-items: baseline;
gap: 1px;
}
/* 三点跳动 */
.translate-dots span {
display: inline-block;
animation: translate-bounce 1.2s ease-in-out infinite;
font-weight: 900;
}
.translate-dots span:nth-child(1) { animation-delay: 0s; }
.translate-dots span:nth-child(2) { animation-delay: 0.2s; }
.translate-dots span:nth-child(3) { animation-delay: 0.4s; }
/* 入场 / 离场过渡 */
.translate-loading-enter-active,
.translate-loading-leave-active {
transition: opacity 0.18s ease, transform 0.18s ease;
}
.translate-loading-enter-from,
.translate-loading-leave-to {
opacity: 0;
transform: scaleX(0.9);
transform-origin: left center;
}
@keyframes translate-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes translate-bounce {
0%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-3px); }
}
</style>
......@@ -11,4 +11,4 @@ export interface TableBatchModalProps {
}
/** 批量操作弹框确认事件类型 */
export type ConfirmEmit = (event: 'confirm', count: number, action: string) => void
export type ConfirmEmit = (event: 'confirm', count: number, action: string, cellChildTags: string[]) => void
......@@ -13,7 +13,8 @@ export function useTableBatchModal(emit: ConfirmEmit, formRef: Ref<FormInst | nu
const pendingAction = ref('')
const form = reactive({
count: 1 as number | null
count: 1 as number | null,
cellChildTags: [] as string[]
})
const rules = {
......@@ -39,6 +40,7 @@ export function useTableBatchModal(emit: ConfirmEmit, formRef: Ref<FormInst | nu
description.value = meta.description
isDelete.value = meta.isDelete
form.count = meta.count
form.cellChildTags = []
pendingAction.value = meta.action
show.value = true
}
......@@ -50,7 +52,7 @@ export function useTableBatchModal(emit: ConfirmEmit, formRef: Ref<FormInst | nu
return
}
if (form.count !== null) {
emit('confirm', form.count, pendingAction.value)
emit('confirm', form.count, pendingAction.value, form.cellChildTags)
show.value = false
}
}
......
<template>
<CommonModal v-model="show" :title="title" :width="360" @confirm="handleConfirm">
<n-form ref="formRef" :model="form" :rules="rules" label-placement="top">
<div class="flex flex-col gap-3">
<span class="text-sm text-color2">{{ description }}</span>
<n-form-item path="count">
<div class="flex flex-col gap-4 py-2">
<n-form-item :label="description" path="count">
<CommonInputNumber
v-model:value="form.count"
:min="1"
......@@ -15,6 +14,17 @@
placeholder="请输入数量"
/>
</n-form-item>
<template v-if="!isDelete">
<n-form-item label="单元格初始化段落节点" path="cellChildTags">
<div class="w-full bg-fill-2 p-3 rounded border border-divider">
<CommonCheckbox
v-model:value="form.cellChildTags"
:options="cellChildOptions"
:space-size="24"
/>
</div>
</n-form-item>
</template>
</div>
</n-form>
</CommonModal>
......@@ -24,10 +34,17 @@
import type { FormInst } from 'naive-ui'
import { useTableBatchModal } from './functionals'
import CommonCheckbox from '@/components/CommonCheckbox.vue'
const emit = defineEmits<{
confirm: [count: number, action: string]
confirm: [count: number, action: string, cellChildTags: string[]]
}>()
const cellChildOptions = [
{ label: 'PARAC (中文)', value: 'PARAC' },
{ label: 'PARA (英文)', value: 'PARA' }
]
const formRef = ref<FormInst | null>(null)
const { show, title, description, isDelete, form, rules, open, handleConfirm } = useTableBatchModal(emit, formRef)
......
import { useEditorStore } from '@/store/editor'
import { useAppStore } from '@/store/app'
import type { XmlNode } from '@/types/xmlNode'
import type { TableCellModel, TableRowModel, TableStructureModel, BatchModalRef, BatchActionMeta, CellParagraphModel } from '../constants'
import { ACTION_META } from '../constants'
......@@ -43,6 +44,7 @@ const setDeepText = (node: XmlNode, text: string): void => {
*/
export function useTableEditor(props: { node: XmlNode }) {
const store = useEditorStore()
const appStore = useAppStore()
const structure = ref<TableStructureModel>({
cols: 0,
......@@ -289,9 +291,10 @@ export function useTableEditor(props: { node: XmlNode }) {
}
}
const createDefaultEntry = (parentRowId: string): XmlNode => {
return {
id: crypto.randomUUID(),
const createDefaultEntry = (parentRowId: string, cellChildTags?: string[] | null): XmlNode => {
const entryId = crypto.randomUUID()
const entry: XmlNode = {
id: entryId,
tagName: 'ENTRY',
attributes: {},
children: [],
......@@ -299,6 +302,30 @@ export function useTableEditor(props: { node: XmlNode }) {
mixedContent: [],
parentId: parentRowId
}
if (cellChildTags && cellChildTags.length > 0) {
// 按照 PARAC -> PARA 的顺序插入,以确保 PARAC 始终在 PARA 节点的前面
const order = ['PARAC', 'PARA']
for (const tag of order) {
if (cellChildTags.includes(tag)) {
const childId = crypto.randomUUID()
const childNode: XmlNode = {
id: childId,
tagName: tag,
attributes: {},
children: [],
textContent: '',
mixedContent: [],
parentId: entryId
}
entry.children.push(childNode)
entry.mixedContent.push({
type: 'element',
nodeId: childId
})
}
}
}
return entry
}
const updateCellText = (node: XmlNode, cellId: string, text: string): void => {
......@@ -329,7 +356,7 @@ export function useTableEditor(props: { node: XmlNode }) {
store.rebuildNodeMap()
}
const addRow = (node: XmlNode, section: 'THEAD' | 'TBODY' = 'TBODY', activeRowId?: string, insertBelow = true): void => {
const addRow = (node: XmlNode, section: 'THEAD' | 'TBODY' = 'TBODY', activeRowId?: string, insertBelow = true, cellChildTags?: string[] | null): void => {
const tgroup = findTgroup(node)
if (!tgroup) return
......@@ -362,7 +389,7 @@ export function useTableEditor(props: { node: XmlNode }) {
}
for (let i = 0; i < cols; i++) {
newRow.children.push(createDefaultEntry(rowId))
newRow.children.push(createDefaultEntry(rowId, cellChildTags))
}
if (activeRowId) {
......@@ -376,6 +403,9 @@ export function useTableEditor(props: { node: XmlNode }) {
insertedRowIds.value.delete(rowId)
}, 5000)
if (appStore.autoExpandOnInsert) {
store.expandNodes([rowId, ...newRow.children.map((c) => c.id)])
}
store.rebuildNodeMap()
return
}
......@@ -388,6 +418,9 @@ export function useTableEditor(props: { node: XmlNode }) {
insertedRowIds.value.delete(rowId)
}, 5000)
if (appStore.autoExpandOnInsert) {
store.expandNodes([rowId, ...newRow.children.map((c) => c.id)])
}
store.rebuildNodeMap()
}
......@@ -415,7 +448,7 @@ export function useTableEditor(props: { node: XmlNode }) {
}
}
const addColumn = (node: XmlNode, activeColIdx?: number, insertRight = true): void => {
const addColumn = (node: XmlNode, activeColIdx?: number, insertRight = true, cellChildTags?: string[] | null): void => {
const tgroup = findTgroup(node)
if (!tgroup) return
......@@ -502,7 +535,7 @@ export function useTableEditor(props: { node: XmlNode }) {
const insertedColCellIds: string[] = []
const createDefaultColEntry = (parentRowId: string): XmlNode => {
const cellNode = createDefaultEntry(parentRowId)
const cellNode = createDefaultEntry(parentRowId, cellChildTags)
insertedColCellIds.push(cellNode.id)
return cellNode
}
......@@ -547,6 +580,9 @@ export function useTableEditor(props: { node: XmlNode }) {
insertedColCellIds.forEach((id) => insertedCellIds.value.delete(id))
}, 5000)
if (appStore.autoExpandOnInsert) {
store.expandNodes(insertedColCellIds)
}
store.rebuildNodeMap()
}
......@@ -1109,7 +1145,7 @@ export function useTableEditor(props: { node: XmlNode }) {
}
])
const handleRowAddSelect = (key: string) => {
const handleRowAddSelect = (key: string, cellChildTags?: string[] | null) => {
let section: 'THEAD' | 'TBODY' = 'TBODY'
let activeRowId: string | undefined = undefined
......@@ -1132,11 +1168,11 @@ export function useTableEditor(props: { node: XmlNode }) {
}
if (key === 'above') {
addRow(props.node, section, activeRowId, false)
addRow(props.node, section, activeRowId, false, cellChildTags)
} else if (key === 'below') {
addRow(props.node, section, activeRowId, true)
addRow(props.node, section, activeRowId, true, cellChildTags)
} else {
addRow(props.node, section)
addRow(props.node, section, undefined, true, cellChildTags)
}
}
......@@ -1157,7 +1193,7 @@ export function useTableEditor(props: { node: XmlNode }) {
}
])
const handleColAddSelect = (key: string) => {
const handleColAddSelect = (key: string, cellChildTags?: string[] | null) => {
let activeColIdx: number | undefined = undefined
if (selectedCellIds.value.length > 0) {
......@@ -1170,11 +1206,11 @@ export function useTableEditor(props: { node: XmlNode }) {
}
if (key === 'left') {
addColumn(props.node, activeColIdx, false)
addColumn(props.node, activeColIdx, false, cellChildTags)
} else if (key === 'right') {
addColumn(props.node, activeColIdx, true)
addColumn(props.node, activeColIdx, true, cellChildTags)
} else {
addColumn(props.node)
addColumn(props.node, undefined, true, cellChildTags)
}
}
......@@ -1470,8 +1506,8 @@ export function useTableEditor(props: { node: XmlNode }) {
export function useTableBatchActions(deps: {
batchModalRef: Ref<BatchModalRef | undefined>
handleContextMenuSelect: (key: string) => void
handleRowAddSelect: (pos: string) => void
handleColAddSelect: (pos: string) => void
handleRowAddSelect: (pos: string, cellChildTags?: string[] | null) => void
handleColAddSelect: (pos: string, cellChildTags?: string[] | null) => void
batchDeleteRows: (rowId: string, count: number) => void
batchDeleteColumns: (colIdx: number, count: number) => void
contextMenu: Ref<{ cellId: string; rowId: string; colIdx: number; show: boolean; x: number; y: number }>
......@@ -1497,7 +1533,7 @@ export function useTableBatchActions(deps: {
}
/** 确认执行批量操作(由 TableBatchModal 的 @confirm 事件触发) */
const executeBatchAction = (count: number, action: string) => {
const executeBatchAction = (count: number, action: string, cellChildTags: string[]) => {
const { cellId, rowId, colIdx } = deps.contextMenu.value
if (cellId && !deps.selectedCellIds.value.includes(cellId)) {
......@@ -1507,22 +1543,22 @@ export function useTableBatchActions(deps: {
switch (action) {
case 'row-above':
for (let i = 0; i < count; i++) deps.handleRowAddSelect('above')
for (let i = 0; i < count; i++) deps.handleRowAddSelect('above', cellChildTags)
break
case 'row-below':
for (let i = 0; i < count; i++) deps.handleRowAddSelect('below')
for (let i = 0; i < count; i++) deps.handleRowAddSelect('below', cellChildTags)
break
case 'row-append':
for (let i = 0; i < count; i++) deps.handleRowAddSelect('append')
for (let i = 0; i < count; i++) deps.handleRowAddSelect('append', cellChildTags)
break
case 'col-left':
for (let i = 0; i < count; i++) deps.handleColAddSelect('left')
for (let i = 0; i < count; i++) deps.handleColAddSelect('left', cellChildTags)
break
case 'col-right':
for (let i = 0; i < count; i++) deps.handleColAddSelect('right')
for (let i = 0; i < count; i++) deps.handleColAddSelect('right', cellChildTags)
break
case 'col-append':
for (let i = 0; i < count; i++) deps.handleColAddSelect('append')
for (let i = 0; i < count; i++) deps.handleColAddSelect('append', cellChildTags)
break
case 'row-delete':
if (rowId) deps.batchDeleteRows(rowId, count)
......
......@@ -41,7 +41,10 @@ import Splitter from './components/Splitter/index.vue'
const themeVars = useThemeVars()
const editorStore = useEditorStore()
const expandedKeys = ref<string[]>([])
const expandedKeys = computed({
get: () => editorStore.expandedKeys,
set: (val) => editorStore.setExpandedKeys(val)
})
// 界面拖动分割状态
const leftWidthPx = ref(560)
......
......@@ -49,7 +49,21 @@ export default defineConfig(({ mode }) => {
base: './', // 打包路径
server: {
port: 5555,
host: true
host: true,
proxy: {
'/translationsManage': {
target: 'https://app.anyremote.cn:8008',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/translationsManage/, '')
},
'/translations': {
target: 'https://app.anyremote.cn:8005',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/translations/, '')
}
}
}
}
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment