Commit 104c0eb9 by pangchong

refactor(settings): 重构偏好设置抽屉组件为组合式结构

- 将 SettingsDrawer 组件拆分至独立子目录,抽取常量、功能函数
- 迁移主题模式、颜色预设、快捷键数据至 constants 文件
- 将状态逻辑封装至 functionals 文件的 useSettingsDrawer 组合函数
- 优化代码结构,提升组件可维护性和复用性
- 替换 TargetNodeItem 组件为组合式实现,拆分逻辑和视图
- 修改 api 响应成功判定,只将 code === 200 视为成功
parent b9291493
......@@ -10,7 +10,7 @@ export const apiConfig = {
if (customConfig && customConfig.apiConfig && customConfig.apiConfig.isSuccess) {
return customConfig.apiConfig.isSuccess(res)
}
// 默认情况下 code 为 200 或 0 时均可认为成功
return res && (res.code === 200 || res.code === 0 || res.code === '200' || res.code === '0')
// 默认情况下 code 为 200 时判定为成功
return res && res.code === 200
}
}
import {
SunnyOutline,
MoonOutline,
PhonePortraitOutline
} from '@vicons/ionicons5'
import type { Component } from 'vue'
export interface ThemeModeItem {
value: string
label: string
icon: Component
}
export interface ColorPresetItem {
label: string
color: string
}
export interface ShortcutItem {
label: string
keys: string[]
}
export const THEME_MODES: ThemeModeItem[] = [
{ value: 'light', label: '浅色', icon: SunnyOutline },
{ value: 'dark', label: '深色', icon: MoonOutline },
{ value: 'system', label: '跟随系统', icon: PhonePortraitOutline }
]
export const COLOR_PRESETS: ColorPresetItem[] = [
{ label: '默认', color: '#165DFF' },
{ label: '紫罗兰', color: '#7C3AED' },
{ label: '樱花粉', color: '#DB2777' },
{ label: '柠檬黄', color: '#D97706' },
{ label: '天蓝色', color: '#0EA5E9' },
{ label: '浅绿色', color: '#10B981' },
{ label: '锌色灰', color: '#71717A' },
{ label: '深绿色', color: '#059669' },
{ label: '深蓝色', color: '#1D4ED8' },
{ label: '橙黄色', color: '#EA580C' },
{ label: '玫瑰红', color: '#E11D48' },
{ label: '中性色', color: '#6B7280' },
{ label: '石板灰', color: '#475569' },
{ label: '中灰色', color: '#9CA3AF' }
]
export const SHORTCUTS: ShortcutItem[] = [
{ label: '切换主题', keys: ['Ctrl', 'Shift', 'D'] },
{ label: '撤销', keys: ['Ctrl', 'Z'] },
{ label: '重做', keys: ['Ctrl', 'Y'] },
{ label: '查找', keys: ['Ctrl', 'F'] },
{ label: '取消 / ESC', keys: ['Escape'] }
]
import { useAppStore } from '@/store/app/index'
export function useSettingsDrawer() {
const themeVars = useThemeVars()
const appStore = useAppStore()
const customColor = ref(appStore.primaryColor)
const currentThemeMode = computed(() => {
return appStore.isDark ? 'dark' : 'light'
})
const setThemeMode = (mode: string) => {
if (mode === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
appStore.isDark = prefersDark
} else {
appStore.isDark = mode === 'dark'
}
}
const applyAndSave = () => {
appStore.applyTheme()
}
const setColor = (color: string) => {
appStore.primaryColor = color
customColor.value = color
applyAndSave()
}
const decreaseFontSize = () => {
appStore.fontSize = Math.max(12, appStore.fontSize - 1)
applyAndSave()
}
const increaseFontSize = () => {
appStore.fontSize = Math.min(20, appStore.fontSize + 1)
applyAndSave()
}
const resetPrefs = () => {
appStore.primaryColor = '#165DFF'
appStore.fontSize = 14
appStore.colorWeak = false
appStore.grayMode = false
appStore.isDark = false
appStore.autoExpandOnInsert = true
appStore.defaultNewColWidth = 0.2
appStore.treeCollapseThreshold = 20
appStore.treeDefaultWidth = 560
appStore.treeMaxRatio = 0.6
customColor.value = '#165DFF'
applyAndSave()
window.$message.success('已重置为默认设置')
}
const copyPrefs = async () => {
const prefs = JSON.stringify(
{
primaryColor: appStore.primaryColor,
fontSize: appStore.fontSize,
isDark: appStore.isDark,
colorWeak: appStore.colorWeak,
grayMode: appStore.grayMode,
autoExpandOnInsert: appStore.autoExpandOnInsert,
defaultNewColWidth: appStore.defaultNewColWidth,
treeCollapseThreshold: appStore.treeCollapseThreshold,
treeDefaultWidth: appStore.treeDefaultWidth,
treeMaxRatio: appStore.treeMaxRatio
},
null,
2
)
try {
await navigator.clipboard.writeText(prefs)
window.$message.success('配置已复制到剪贴板')
} catch (err) {
window.$message.error('复制失败')
}
}
const importPrefs = async () => {
try {
const text = await navigator.clipboard.readText()
if (!text) {
window.$message.warning('剪贴板中没有内容')
return
}
const conf = JSON.parse(text)
// 验证关键字段
if (conf.primaryColor) appStore.primaryColor = conf.primaryColor
if (typeof conf.fontSize === 'number') appStore.fontSize = conf.fontSize
if (typeof conf.isDark === 'boolean') appStore.isDark = conf.isDark
if (typeof conf.colorWeak === 'boolean') appStore.colorWeak = conf.colorWeak
if (typeof conf.grayMode === 'boolean') appStore.grayMode = conf.grayMode
if (typeof conf.autoExpandOnInsert === 'boolean') appStore.autoExpandOnInsert = conf.autoExpandOnInsert
if (typeof conf.defaultNewColWidth === 'number') appStore.defaultNewColWidth = conf.defaultNewColWidth
if (typeof conf.treeCollapseThreshold === 'number') appStore.treeCollapseThreshold = conf.treeCollapseThreshold
if (typeof conf.treeDefaultWidth === 'number') appStore.treeDefaultWidth = conf.treeDefaultWidth
if (typeof conf.treeMaxRatio === 'number') appStore.treeMaxRatio = conf.treeMaxRatio
applyAndSave()
window.$message.success('偏好设置已成功导入并应用')
} catch (err) {
window.$message.error('导入失败:请确保剪贴板内容是有效的 JSON 格式')
}
}
onMounted(() => {
applyAndSave()
})
return {
themeVars,
appStore,
customColor,
currentThemeMode,
setThemeMode,
setColor,
applyAndSave,
decreaseFontSize,
increaseFontSize,
resetPrefs,
copyPrefs,
importPrefs
}
}
......@@ -59,7 +59,7 @@
<div class="text-sm font-semibold mb-3" :style="{ color: themeVars.textColor2 }">主题</div>
<div class="grid grid-cols-3 gap-2">
<div
v-for="mode in themeModes"
v-for="mode in THEME_MODES"
:key="mode.value"
class="flex flex-col items-center gap-1.5 p-3 rounded-lg cursor-pointer border-2 transition-all"
:style="{
......@@ -88,7 +88,7 @@
<div class="text-sm font-semibold mb-3" :style="{ color: themeVars.textColor2 }">内置主题</div>
<div class="grid grid-cols-3 gap-2">
<div
v-for="preset in colorPresets"
v-for="preset in COLOR_PRESETS"
:key="preset.color"
class="flex flex-col items-center gap-1.5 p-2 rounded-lg cursor-pointer border-2 transition-all"
:style="{
......@@ -260,7 +260,7 @@
<!-- 快捷键 tab -->
<n-tab-pane name="shortcuts" tab="快捷键">
<div class="space-y-3">
<div v-for="s in shortcuts" :key="s.label" class="flex items-center justify-between">
<div v-for="s in SHORTCUTS" :key="s.label" class="flex items-center justify-between">
<span class="text-sm" :style="{ color: themeVars.textColor2 }">{{ s.label }}</span>
<div class="flex gap-1">
<span
......@@ -306,9 +306,6 @@
<script setup lang="ts">
import {
SettingsOutline,
SunnyOutline,
MoonOutline,
PhonePortraitOutline,
ReloadOutline,
PinOutline,
RemoveOutline,
......@@ -316,147 +313,21 @@ import {
CopyOutline,
DownloadOutline
} from '@vicons/ionicons5'
import { useAppStore } from '@/store/app/index'
const themeVars = useThemeVars()
const appStore = useAppStore()
const customColor = ref(appStore.primaryColor)
const currentThemeMode = computed(() => {
return appStore.isDark ? 'dark' : 'light'
})
const themeModes = [
{ value: 'light', label: '浅色', icon: SunnyOutline },
{ value: 'dark', label: '深色', icon: MoonOutline },
{ value: 'system', label: '跟随系统', icon: PhonePortraitOutline }
]
const colorPresets = [
{ label: '默认', color: '#165DFF' },
{ label: '紫罗兰', color: '#7C3AED' },
{ label: '樱花粉', color: '#DB2777' },
{ label: '柠檬黄', color: '#D97706' },
{ label: '天蓝色', color: '#0EA5E9' },
{ label: '浅绿色', color: '#10B981' },
{ label: '锌色灰', color: '#71717A' },
{ label: '深绿色', color: '#059669' },
{ label: '深蓝色', color: '#1D4ED8' },
{ label: '橙黄色', color: '#EA580C' },
{ label: '玫瑰红', color: '#E11D48' },
{ label: '中性色', color: '#6B7280' },
{ label: '石板灰', color: '#475569' },
{ label: '中灰色', color: '#9CA3AF' }
]
const shortcuts = [
{ label: '切换主题', keys: ['Ctrl', 'Shift', 'D'] },
{ label: '撤销', keys: ['Ctrl', 'Z'] },
{ label: '重做', keys: ['Ctrl', 'Y'] },
{ label: '查找', keys: ['Ctrl', 'F'] },
{ label: '取消 / ESC', keys: ['Escape'] }
]
const setThemeMode = (mode: string) => {
if (mode === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
appStore.isDark = prefersDark
} else {
appStore.isDark = mode === 'dark'
}
}
const setColor = (color: string) => {
appStore.primaryColor = color
customColor.value = color
applyAndSave()
}
const applyAndSave = () => {
appStore.applyTheme()
}
const decreaseFontSize = () => {
appStore.fontSize = Math.max(12, appStore.fontSize - 1)
applyAndSave()
}
const increaseFontSize = () => {
appStore.fontSize = Math.min(20, appStore.fontSize + 1)
applyAndSave()
}
const resetPrefs = () => {
appStore.primaryColor = '#165DFF'
appStore.fontSize = 14
appStore.colorWeak = false
appStore.grayMode = false
appStore.isDark = false
appStore.autoExpandOnInsert = true
appStore.defaultNewColWidth = 0.2
appStore.treeCollapseThreshold = 20
appStore.treeDefaultWidth = 560
appStore.treeMaxRatio = 0.6
customColor.value = '#165DFF'
applyAndSave()
window.$message.success('已重置为默认设置')
}
const copyPrefs = async () => {
const prefs = JSON.stringify(
{
primaryColor: appStore.primaryColor,
fontSize: appStore.fontSize,
isDark: appStore.isDark,
colorWeak: appStore.colorWeak,
grayMode: appStore.grayMode,
autoExpandOnInsert: appStore.autoExpandOnInsert,
defaultNewColWidth: appStore.defaultNewColWidth,
treeCollapseThreshold: appStore.treeCollapseThreshold,
treeDefaultWidth: appStore.treeDefaultWidth,
treeMaxRatio: appStore.treeMaxRatio
},
null,
2
)
try {
await navigator.clipboard.writeText(prefs)
window.$message.success('配置已复制到剪贴板')
} catch (err) {
window.$message.error('复制失败')
}
}
const importPrefs = async () => {
try {
const text = await navigator.clipboard.readText()
if (!text) {
window.$message.warning('剪贴板中没有内容')
return
}
const conf = JSON.parse(text)
// 验证关键字段
if (conf.primaryColor) appStore.primaryColor = conf.primaryColor
if (typeof conf.fontSize === 'number') appStore.fontSize = conf.fontSize
if (typeof conf.isDark === 'boolean') appStore.isDark = conf.isDark
if (typeof conf.colorWeak === 'boolean') appStore.colorWeak = conf.colorWeak
if (typeof conf.grayMode === 'boolean') appStore.grayMode = conf.grayMode
if (typeof conf.autoExpandOnInsert === 'boolean') appStore.autoExpandOnInsert = conf.autoExpandOnInsert
if (typeof conf.defaultNewColWidth === 'number') appStore.defaultNewColWidth = conf.defaultNewColWidth
if (typeof conf.treeCollapseThreshold === 'number') appStore.treeCollapseThreshold = conf.treeCollapseThreshold
if (typeof conf.treeDefaultWidth === 'number') appStore.treeDefaultWidth = conf.treeDefaultWidth
if (typeof conf.treeMaxRatio === 'number') appStore.treeMaxRatio = conf.treeMaxRatio
applyAndSave()
window.$message.success('偏好设置已成功导入并应用')
} catch (err) {
window.$message.error('导入失败:请确保剪贴板内容是有效的 JSON 格式')
}
}
onMounted(() => {
applyAndSave()
})
import { THEME_MODES, COLOR_PRESETS, SHORTCUTS } from './constants'
import { useSettingsDrawer } from './functionals'
const {
themeVars,
appStore,
customColor,
currentThemeMode,
setThemeMode,
setColor,
applyAndSave,
decreaseFontSize,
increaseFontSize,
resetPrefs,
copyPrefs,
importPrefs
} = useSettingsDrawer()
</script>
export interface TargetItem {
id: string
tagName: string
suffix: string
displayName: string
/** 完整未截断的内容,供原生 title 属性悬停时展示 */
tooltip?: string
pathString: string
depth: number
}
export interface TargetNodeItemProps {
item: TargetItem
isSelected?: boolean
}
import type { TargetNodeItemProps } from '../constants'
export function useTargetNodeItem(props: TargetNodeItemProps) {
const itemTitle = computed(() => {
return props.item.tooltip || props.item.displayName
})
const paddingLeftStyle = computed(() => {
return `${(props.item.depth || 0) * 12 + 12}px`
})
return {
itemTitle,
paddingLeftStyle
}
}
......@@ -2,8 +2,8 @@
<div
class="target-node-item"
:class="{ 'is-selected': isSelected }"
:style="{ paddingLeft: `${(item.depth || 0) * 12 + 12}px` }"
:title="item.tooltip || item.displayName"
:style="{ paddingLeft: paddingLeftStyle }"
:title="itemTitle"
>
<span class="check-space">
<span v-if="isSelected" class="check-icon"></span>
......@@ -14,21 +14,11 @@
</template>
<script setup lang="ts">
export interface TargetItem {
id: string
tagName: string
suffix: string
displayName: string
/** 完整未截断的内容,供原生 title 属性悬停时展示 */
tooltip?: string
pathString: string
depth: number
}
import type { TargetNodeItemProps } from './constants'
import { useTargetNodeItem } from './functionals'
defineProps<{
item: TargetItem
isSelected?: boolean
}>()
const props = defineProps<TargetNodeItemProps>()
const { itemTitle, paddingLeftStyle } = useTargetNodeItem(props)
</script>
<style scoped>
......
import type { DropdownOption } from 'naive-ui'
import TargetNodeItem from '../components/TargetNodeItem.vue'
import type { TargetItem } from '../components/TargetNodeItem.vue'
import TargetNodeItem from '../components/TargetNodeItem/index.vue'
import type { TargetItem } from '../components/TargetNodeItem/constants'
import {
CreateOutline,
CopyOutline,
......@@ -239,7 +239,7 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
const node = activeNode.value
if (!node) return '未知'
if (node.tagName === '#text') return '#text'
return node.tagName
return `<${node.tagName}>`
})
// 是否是文本节点
......@@ -934,7 +934,7 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
},
`目标: ${activeNodeName.value}`
),
key: 'headerTarget',
key: `headerTarget:${activeNodeId.value}`,
icon: icon(LayersOutline),
children: levelChildren.length > 1 ? levelChildren : undefined
})
......
......@@ -94,9 +94,9 @@
<!-- Meta 行 -->
<div class="flex items-center justify-between text-[10px] text-color3 select-none">
<div class="flex items-center gap-1.5 flex-wrap">
<n-tag size="tiny" :type="item.type === 'full' ? 'info' : 'warning'" :bordered="false" round>
<CommonTag size="tiny" :type="item.type === 'full' ? 'info' : 'warning'" :bordered="false" round>
{{ item.type === 'full' ? '完整XML' : '片段' }}
</n-tag>
</CommonTag>
<span class="font-mono bg-fill-2 px-1.5 py-0.5 rounded text-color2">&lt;{{ item.xmlNode.tagName }}&gt;</span>
</div>
<span class="font-mono">{{ item.time }}</span>
......
......@@ -529,7 +529,7 @@ import {
} from '@vicons/ionicons5'
import { useEditorToolbar } from './functionals'
import { GREEN_BUTTONS } from './constants'
import SettingsDrawer from '@/layouts/components/SettingsDrawer.vue'
import SettingsDrawer from '@/layouts/components/SettingsDrawer/index.vue'
import InsertFragmentModal from './components/InsertFragmentModal/index.vue'
import CreateTableModal from './components/CreateTableModal/index.vue'
import CreateSignoffModal from './components/CreateSignoffModal/index.vue'
......
......@@ -42,11 +42,11 @@
: item.level === 0
? 'text-primary dark:text-primary-hover font-bold'
: item.tagName === 'WARNING' || item.tagName === 'CAUTION'
? 'text-amber-500 dark:text-amber-400'
? 'text-warning'
: item.tagName === 'GRAPHIC'
? 'text-indigo-500 dark:text-indigo-400'
? 'text-primary'
: item.tagName === 'TABLE'
? 'text-emerald-500 dark:text-emerald-400'
? 'text-success'
: 'text-primary/70'
]"
>
......
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