Commit 2e5e0bef by pangchong

refactor(editor): 重构编辑器桥接模块并新增单选复选框组件

- 新增 CommonCheckboxSingle 组件,替代项目中所有单体复选框使用,支持 checked-value 等自定义属性及严格模式
- 统一替换所有业务页面中的单个 n-checkbox 组件为 CommonCheckboxSingle 以保证样式和行为一致
- 重构 bridge.ts,支持动态加载定制目录下的自定义桥接实现,优先调用定制逻辑,提升扩展性
- 在 useEditor 逻辑中集成桥接事件监听,支持父级请求获取 XML 和 PDF 数据,同步工卡编辑器数据状态
- 优化 api/index.ts 中 response 处理,增加支持 xml、html、text 等多种响应体格式的返回方案
- 调整 vite 配置及脚本命令,新增 dev:xm 模式并修正部分环境变量配置以支持多分区运行
- 更新 .env、.gemini/.rules.md 和接口调用方式,规范组件使用及接口调用事件的命名和格式
parent 7fa140dc
VITE_PARTITION_NAME = from_project_001
\ No newline at end of file
# 分区名称标识
VITE_PARTITION_NAME = from_project_001
# 开发环境下默认启用本地开发桥接服务 (true 使用本地接口直接请求/模拟,false 降级使用父项目传递的 promise)
VITE_IF_LOCAL_BRIDGE = true
\ No newline at end of file
# 自定义定制代码目录 (例如 'xm')
VITE_CUSTOM_CODE = xm
# 分区名称标识
VITE_PARTITION_NAME = from_project_001
# 后端 API 目标服务基准地址
VITE_API_URL = https://amro-vue.anyremote.cn
# 接口前缀路径
VITE_API_PREFIX_URL = /api/v1
# 是否使用本地开发桥接服务 (true 使用本地接口直接请求,false 降级使用父项目传递的 promise)
VITE_IF_LOCAL_BRIDGE = true
\ No newline at end of file
VITE_API_PREFIX_URL = /api/v1
\ No newline at end of file
......@@ -212,7 +212,9 @@
- **统一使用全局组件**:在所有业务界面中,对于选择框、多选框、数字输入框与状态标签,**禁止**直接使用 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>` 全局组件的使用。
- **多选与单选复选框**:在所有业务页面中,**禁用** Naive UI 原生的 `<n-checkbox>` `<n-checkbox-group>`。根据不同场景选用以下封装组件:
1. **多选复选框组**:使用 **`CommonCheckbox`** 替代 `n-checkbox-group` 及原生的 `v-for` 循环列表。
2. **单体复选框(布尔值/独立开关)**:使用 **`CommonCheckboxSingle`** 替代单个 `<n-checkbox>`,可通过 `checked-value``unchecked-value` 属性直接映射所需的任意选中值类型(如字符 `'Y'`/`'N'` 等),并支持 `strict` 属性进行非严格模式下的类型自动转换比对。
- **状态标签**:使用 **`CommonTag`** 替代 `n-tag`。支持插槽、字典配置及 autoColor 自适应着色。
- **禁用冗余的 placeholder 占位符**
- 在编写 `CommonSelect` `n-input` 组件时,对于普通属性、枚举选择或内容输入,**禁止**手动添加 `placeholder="请选择"``placeholder="选择"` `placeholder="请输入"` 等冗余的占位字符。
......
......@@ -4,12 +4,9 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev:prod": "vite --mode prod",
"dev:test": "vite --mode test",
"dev": "vite --mode dev",
"dev:xm": "vite --mode xm",
"build": "vue-tsc -b && vite build",
"build:prod": "vue-tsc -b && vite build --mode prod",
"build": "vue-tsc -b && vite build --mode dev",
"build:xm": "vue-tsc -b && vite build --mode xm",
"preview": "vite preview",
"prepare": "husky install",
......
......@@ -45,10 +45,9 @@ const createService = (baseURL: string) => {
const contentType = response.headers.get('content-type') || ''
const isJson = contentType.includes('application/json')
let json: ResponseData | null = null
if (isJson) {
json = (await response.json()) as ResponseData
const json = (await response.json()) as ResponseData
if (json) {
if (
json.code === 200 ||
......@@ -58,9 +57,15 @@ const createService = (baseURL: string) => {
(json.code === undefined && json.success === undefined)
) {
json.code = 200
return json
}
return json
}
} else if (
contentType.includes('xml') ||
contentType.includes('text/html') ||
contentType.includes('text/plain')
) {
return await response.text()
} else {
const isBinary =
method.meta?.isDownload ||
......@@ -77,23 +82,25 @@ const createService = (baseURL: string) => {
}
}
if (!json) {
json = (await response.json()) as ResponseData
}
if (json) {
if (
json.code === 200 ||
json.code === '200' ||
json.success === true ||
json.success === 'true' ||
(json.code === undefined && json.success === undefined)
) {
json.code = 200
// 兜底尝试作为 JSON 解析
try {
const json = (await response.json()) as ResponseData
if (json) {
if (
json.code === 200 ||
json.code === '200' ||
json.success === true ||
json.success === 'true' ||
(json.code === undefined && json.success === undefined)
) {
json.code = 200
}
}
return json
} catch (e) {
// 若解析 JSON 失败且无特定类型,返回原始文本
return await response.text()
}
return json!
},
onError: (err, method) => {
if (method.meta?.showLoading) {
......
<template>
<n-checkbox
:checked="computedChecked"
:disabled="disabled"
:indeterminate="indeterminate"
v-bind="$attrs"
@update:checked="handleUpdateChecked"
>
<slot>{{ label }}</slot>
</n-checkbox>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps({
/** 绑定的值 (如 v-model) */
checked: {
type: [Boolean, String, Number],
default: undefined
},
value: {
type: [Boolean, String, Number],
default: undefined
},
modelValue: {
type: [Boolean, String, Number],
default: undefined
},
/** 选中状态对应的值,默认是 true */
checkedValue: {
type: [Boolean, String, Number],
default: true
},
/** 未选中状态对应的值,默认是 false */
uncheckedValue: {
type: [Boolean, String, Number],
default: false
},
/** 严格模式:默认不开启(不开启时自动转字符串比对,不区分 1 和 '1') */
strict: {
type: Boolean,
default: false
},
/** 禁用状态 */
disabled: {
type: Boolean,
default: false
},
/** 半选状态 */
indeterminate: {
type: Boolean,
default: false
},
/** 显示的文本标签 */
label: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:checked', 'update:value', 'update:modelValue', 'change'])
/**
* 值的标准化处理
*/
const normalizeValue = (v: any) => {
if (v === null || v === undefined || v === '') return null
return props.strict ? v : String(v)
}
/**
* 计算复选框是否被勾选
*/
const computedChecked = computed(() => {
// 依次判断绑定的属性
const val = props.modelValue !== undefined
? props.modelValue
: (props.value !== undefined ? props.value : props.checked)
if (val === undefined || val === null) {
return false
}
return normalizeValue(val) === normalizeValue(props.checkedValue)
})
/**
* 状态更新处理函数
*/
const handleUpdateChecked = (isCurrentlyChecked: boolean) => {
const newValue = isCurrentlyChecked ? props.checkedValue : props.uncheckedValue
emit('update:checked', isCurrentlyChecked)
emit('update:value', newValue)
emit('update:modelValue', newValue)
emit('change', newValue)
}
</script>
<style scoped></style>
......@@ -40,7 +40,7 @@
@animationend="onAnimationEnd"
>
<!-- 2. Checkbox 模式 -->
<n-checkbox
<CommonCheckboxSingle
v-if="mode === 'checkbox'"
:checked="item.checked"
:disabled="item.disabled"
......
import { execute } from '@/utils/bridgeHelper'
import { useEditorStore } from '@/store/editor'
// 动态加载所有定制目录下的 bridge.ts 文件
const customBridges = import.meta.glob<any>('../**/utils/bridge.ts', { eager: true })
const customCode = import.meta.env.VITE_CUSTOM_CODE
const customPath = customCode ? `../${customCode}/utils/bridge.ts` : ''
const customBridge = customPath ? customBridges[customPath] : null
export interface TemplateItem {
id: number
......@@ -31,20 +37,95 @@ const localBridge = {
uploadImage: async (file: File): Promise<any> => {
return null
},
getEditorData: async (): Promise<string> => {
return ''
}
}
// 默认的方法实现
const defaultGetTemplateList = execute('GET_TEMPLATE_LIST', localBridge.getTemplateList, (page, rows) => ({ page, rows }))
const defaultSaveTemplate = execute('SAVE_AS_TEMPLATE', localBridge.saveTemplate, (name, content) => ({ content }))
const defaultUploadImage = execute('UPLOAD_EDITOR_FILE', localBridge.uploadImage, (file) => ({ file }))
const defaultGetEditorData = execute('GET_EDITOR_DATA', localBridge.getEditorData, () => ({}))
/**
* 获取模板列表
*/
export const getTemplateList = execute('getTemplateList', localBridge.getTemplateList, (page, rows) => ({ page, rows }))
export const getTemplateList = (...args: any[]) => {
if (customBridge && customBridge.getTemplateList) {
return customBridge.getTemplateList(...args)
}
return defaultGetTemplateList(...(args as [number, number]))
}
/**
* 保存模板
*/
export const saveTemplate = execute('saveTemplate', localBridge.saveTemplate, (name, content) => ({ name, content }))
export const saveTemplate = (...args: any[]) => {
if (customBridge && customBridge.saveTemplate) {
return customBridge.saveTemplate(...args)
}
return defaultSaveTemplate(...(args as [string, string]))
}
/**
* 上传工卡图片资源
*/
export const uploadImage = execute('uploadImage', localBridge.uploadImage, (file) => ({ file }))
export const uploadImage = (...args: any[]) => {
if (customBridge && customBridge.uploadImage) {
return customBridge.uploadImage(...args)
}
return defaultUploadImage(...(args as [File]))
}
/**
* 获取工卡 XML 数据
*/
export const getEditorData = (...args: any[]) => {
if (customBridge && customBridge.getEditorData) {
return customBridge.getEditorData(...args)
}
return defaultGetEditorData()
}
/**
* 注册编辑器的桥接事件监听器 (用于集成模式下响应父级提取 XML/PDF 数据的请求)
* 返回一个清理函数,用于组件卸载时注销所有监听器
*/
export const registerEditorBridgeListeners = (): (() => void) => {
if (customBridge && customBridge.registerEditorBridgeListeners) {
return customBridge.registerEditorBridgeListeners()
}
if (isLocalBridge()) return () => {}
const store = useEditorStore()
const postMessageHelper = usePostMessage()
// 监听来自父级的 XML 保存请求并响应当前编辑器中的 XML 数据
const unXml = postMessageHelper.on('GET_EDITOR_GXXML', () => {
if (store.xmlTree) {
const xml = serializeTreeToXml(store.xmlTree, 0, true)
postMessageHelper.emit('GET_EDITOR_GXXML', { jcXml: xml })
}
})
// 监听来自父级的 PDF 发布/导出请求并响应当前编辑器中的 XML 数据
const unPdf = postMessageHelper.on('GET_EDITOR_GXPDF', () => {
if (store.xmlTree) {
const xml = serializeTreeToXml(store.xmlTree, 0, true)
postMessageHelper.emit('GET_EDITOR_GXPDF', {
jcXml: xml,
xmlContent: xml,
content: xml
})
}
})
return () => {
unXml()
unPdf()
}
}
import { usePostMessage } from '@/hooks/usePostMessage'
/**
* 判断当前是否使用本地开发桥接服务
*/
......@@ -40,6 +38,10 @@ export const callBridge = async <T = any>(methodName: string, params?: any, ...a
// 3. 降级使用 postMessage RPC 进行跨域通信
const postMessageHelper = usePostMessage()
if (methodName.toUpperCase() === 'SAVE_AS_TEMPLATE') {
postMessageHelper.emit(methodName, params)
return null as any
}
return await postMessageHelper.call<T>(methodName, params)
}
......
......@@ -944,7 +944,7 @@
:key="item.id"
class="flex items-center space-x-2 bg-fill-1 px-3 py-1.5 rounded border border-divider shadow-sm hover:border-primary transition-all"
>
<n-checkbox :checked="false" disabled />
<CommonCheckboxSingle :checked="false" disabled />
<span
contenteditable="true"
class="text-xs text-color2 focus:outline-none focus:bg-fill-3 px-1 rounded"
......
......@@ -7,8 +7,8 @@
<div v-if="!isSheetOnly" class="p-3 bg-fill-2 border border-divider rounded-lg">
<div class="font-bold text-xs text-color2 mb-2">图片 (GRAPHIC) 可选节点</div>
<div class="flex flex-col gap-2">
<n-checkbox v-model:checked="form.hasGraphicEffect">包含适用性说明 &lt;EFFECT&gt;</n-checkbox>
<n-checkbox v-model:checked="form.hasGraphicTitle">包含附图标题 &lt;TITLE&gt;</n-checkbox>
<CommonCheckboxSingle v-model:checked="form.hasGraphicEffect">包含适用性说明 &lt;EFFECT&gt;</CommonCheckboxSingle>
<CommonCheckboxSingle v-model:checked="form.hasGraphicTitle">包含附图标题 &lt;TITLE&gt;</CommonCheckboxSingle>
</div>
</div>
......@@ -16,8 +16,8 @@
<div class="p-3 bg-fill-2 border border-divider rounded-lg">
<div class="font-bold text-xs text-color2 mb-2">图纸 (SHEET) 可选节点</div>
<div class="flex flex-col gap-2">
<n-checkbox v-model:checked="form.hasSheetEffect">包含适用性说明 &lt;EFFECT&gt;</n-checkbox>
<n-checkbox v-model:checked="form.hasSheetTitle">包含图纸标题 &lt;TITLE&gt;</n-checkbox>
<CommonCheckboxSingle v-model:checked="form.hasSheetEffect">包含适用性说明 &lt;EFFECT&gt;</CommonCheckboxSingle>
<CommonCheckboxSingle v-model:checked="form.hasSheetTitle">包含图纸标题 &lt;TITLE&gt;</CommonCheckboxSingle>
</div>
</div>
......
......@@ -31,7 +31,7 @@
<n-icon><checkmark-circle-outline /></n-icon>
可安全删除的节点 ({{ safeNodes.length }} 个):
</span>
<n-checkbox :checked="isAllChecked" :indeterminate="isIndeterminate" @update:checked="toggleCheckAll">全选</n-checkbox>
<CommonCheckboxSingle :checked="isAllChecked" :indeterminate="isIndeterminate" @update:checked="toggleCheckAll">全选</CommonCheckboxSingle>
</div>
<CommonNodeDetailList
:items="formattedSafeNodes"
......
......@@ -100,7 +100,7 @@
</div>
</Transition>
<!-- Checkbox (批量管理模式) -->
<n-checkbox
<CommonCheckboxSingle
v-if="isBatchMode"
:checked="selectedKeys.has(item.id)"
class="absolute left-2.5 shrink-0 z-10"
......
import { useEditorStore } from '@/store/editor'
import type { XmlNode } from '@/types/xmlNode'
import { DEFAULT_FILE_NAME } from '../constants'
// 静态导入 DTD JSON
import dtdJson from '@/assets/json/dtd.json'
// 导入原始 XML 文本 (?raw)
......@@ -12,12 +11,31 @@ import xmlText from '@/assets/file/AMEA-A282400-02-1_0_0.xml?raw'
*/
export function useEditor() {
const store = useEditorStore()
const cleanupFns: (() => void)[] = []
// 注册桥接通信事件监听 (GET_EDITOR_GXXML 和 GET_EDITOR_GXPDF)
const unregisterBridge = registerEditorBridgeListeners()
cleanupFns.push(unregisterBridge)
onUnmounted(() => {
cleanupFns.forEach((un) => un())
})
const initialize = async (): Promise<void> => {
window.$loading?.show('加载数据中…')
try {
loadDtdSchema(dtdJson as any)
const tree = await parseXmlToTreeAsync(xmlText)
let xmlContent = xmlText
try {
const remoteXml = await getEditorData()
if (remoteXml) {
console.log('获取到的工卡 XML 数据:\n', remoteXml)
xmlContent = remoteXml
}
} catch (err) {
console.warn('获取工卡初始化数据失败,使用本地默认模板文件:', err)
}
const tree = await parseXmlToTreeAsync(xmlContent)
store.setXmlTree(tree)
} catch (e: any) {
window.$notification.error(e.message || '加载 XML 配置文件出错,请检查语法', {
......
import { apiService } from '@/api'
import { execute } from '@/utils/bridgeHelper'
export interface TemplateItem {
id: number
name: string
......@@ -48,20 +45,43 @@ const localBridge = {
file,
jcpkid: '13054174'
})
},
getEditorData: async (): Promise<string> => {
const res = await apiService.postForm<any>('/plugins/GET_JCCEPFILE_TO_ONLINE_EDITOR', {
jcpkid: '13054174'
})
if (typeof res === 'string') {
return res
}
if (res && typeof res === 'object') {
if (typeof res.data === 'string') {
return res.data
}
if (res.data && typeof res.data.content === 'string') {
return res.data.content
}
}
return ''
}
}
/**
* 获取模板列表
*/
export const getTemplateList = execute('getTemplateList', localBridge.getTemplateList, (page, rows) => ({ page, rows }))
export const getTemplateList = execute('GET_TEMPLATE_LIST', localBridge.getTemplateList, (page, rows) => ({ page, rows }))
/**
* 保存模板
*/
export const saveTemplate = execute('saveTemplate', localBridge.saveTemplate, (name, content) => ({ name, content }))
export const saveTemplate = execute('SAVE_AS_TEMPLATE', localBridge.saveTemplate, (name, content) => ({ content }))
/**
* 上传工卡图片资源
*/
export const uploadImage = execute('uploadImage', localBridge.uploadImage, (file) => ({ file }))
export const uploadImage = execute('UPLOAD_EDITOR_FILE', localBridge.uploadImage, (file) => ({ file }))
/**
* 获取工卡 XML 数据
*/
export const getEditorData = execute('GET_EDITOR_DATA', localBridge.getEditorData, () => ({}))
......@@ -8,14 +8,10 @@ import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
const customCode = env.VITE_CUSTOM_CODE || ''
const apiUrl = env.VITE_API_URL
return {
resolve: {
alias: {
'@/utils/bridge': customCode
? path.resolve(__dirname, `./src/${customCode}/utils/bridge`)
: path.resolve(__dirname, './src/utils/bridge'),
'@': path.resolve(__dirname, './src')
}
},
......@@ -72,7 +68,7 @@ export default defineConfig(({ mode }) => {
changeOrigin: true,
secure: false,
headers: {
Cookie: '_udid=de1f23e4-c072-42b9-a3f8-5f91d859b434; i18next=zh; JSESSIONID=16EA8A4AC66DD35455A3DACE83FA56B6; _amro_sk=ac11efbe-0a7b-467d-b597-212d5b0069cc'
Cookie: '_udid=23a51873-35b9-4cf9-b676-ce04ab9fef35; JSESSIONID=AD12C8D556906E98F16EBAC56B0E1ECA; _amro_sk=dc32fa12-7d12-4235-960f-189a36eeac98'
}
},
'/mnt': {
......
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