Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | <script setup>
import { ref, computed, watch, onMounted } from 'vue'
import Splitter from '@/components/ui/splitter/splitter.vue'
import SplitterPanel from '@/components/ui/splitter/panel.vue'
import Spin from '@/components/ui/spin/spin.vue'
import { loadDataApi } from '@/services/api'
import FileCategoryPanel from './FileCategoryPanel.vue'
import FileListPanel from './FileListPanel.vue'
import FilePreviewPanel from './FilePreviewPanel.vue'
import { useFileCategory } from './composables/useFileCategory'
const props = defineProps({
moduleCode: { type: [String, Number], required: true },
father: { type: [String, Number], required: true },
showTree: { type: Boolean, default: true },
readonly: { type: Boolean, default: false },
initialCateId: { type: String, default: '' },
})
const emit = defineEmits(['uploaded', 'deleted', 'selected'])
// ---------- 模块解析 ----------
const moduleId = ref('')
const moduleResolving = ref(false)
const moduleError = ref('')
async function resolveModule() {
const code = props.moduleCode
if (!code) {
moduleId.value = ''
return
}
moduleResolving.value = true
moduleError.value = ''
try {
const res = await loadDataApi(
's_module',
`b_code = N'${String(code).replaceAll("'", "''")}' AND b_canuse = 1`,
'',
['b_id', 'b_code', 'b_name'],
)
if (res.data?.length) {
moduleId.value = String(res.data[0].b_id)
} else {
moduleId.value = ''
moduleError.value = `未找到模块 "${code}"`
}
} catch (e) {
moduleError.value = e?.message || '模块解析失败'
moduleId.value = ''
} finally {
moduleResolving.value = false
}
}
watch(
() => props.moduleCode,
() => resolveModule(),
)
// ---------- 分类 ----------
const {
treeData: categoryTreeData,
loading: categoryLoading,
selectedKey: categorySelectedKey,
loadCategories,
selectCategory,
} = useFileCategory()
const activeCateId = computed(() => {
const key = categorySelectedKey.value
if (!key || key === '__fms_tree_root__') return ''
return key
})
const categoryExpandedKeys = ref(['__fms_tree_root__'])
function onCategorySelect(key) {
selectCategory(key)
}
// ---------- 预览选中 ----------
const fileListRef = ref(null)
const selectedFile = ref(null)
const currentFileList = computed(() => fileListRef.value?.files || [])
function onPreviewFile(file) {
selectedFile.value = file
emit('selected', file)
}
function onPreviewPrev() {
const list = currentFileList.value
const idx = list.findIndex((f) => f.subid === selectedFile.value?.subid)
if (idx > 0) {
selectedFile.value = list[idx - 1]
emit('selected', selectedFile.value)
}
}
function onPreviewNext() {
const list = currentFileList.value
const idx = list.findIndex((f) => f.subid === selectedFile.value?.subid)
if (idx >= 0 && idx < list.length - 1) {
selectedFile.value = list[idx + 1]
emit('selected', selectedFile.value)
}
}
// ---------- 事件代理 ----------
function onUploaded() {
emit('uploaded')
}
function onDeleted() {
selectedFile.value = null
emit('deleted')
}
// ---------- 挂载 ----------
onMounted(() => {
resolveModule()
loadCategories()
if (props.initialCateId) {
selectCategory(props.initialCateId)
categoryExpandedKeys.value = ['__fms_tree_root__', String(props.initialCateId)]
}
})
const ready = computed(() => !moduleResolving.value && !moduleError.value)
defineExpose({ moduleId, refreshCategories: loadCategories })
</script>
<template>
<div class="ff-container">
<div v-if="moduleResolving" class="ff-status">
<Spin />
<span class="ff-status__text">正在解析模块...</span>
</div>
<div v-else-if="moduleError" class="ff-status ff-status--error">
{{ moduleError }}
</div>
<template v-else>
<Splitter class="ff-splitter">
<!-- 左:分类树(固定 220px 侧栏) -->
<SplitterPanel
v-if="showTree"
:default-size="220"
:min="160"
:max="320"
>
<FileCategoryPanel
:tree-data="categoryTreeData"
:loading="categoryLoading"
:selected-key="categorySelectedKey"
:expanded-keys="categoryExpandedKeys"
@select="onCategorySelect"
@update:expanded-keys="(keys) => (categoryExpandedKeys = keys)"
/>
</SplitterPanel>
<!-- 中:文件列表(固定 500px) -->
<SplitterPanel :default-size="500" :min="280">
<div class="ff-center">
<FileListPanel
ref="fileListRef"
:module-code="moduleCode"
:module-id="moduleId"
:father="father"
:cate-id="activeCateId"
:readonly="readonly"
@preview="onPreviewFile"
@uploaded="onUploaded"
@deleted="onDeleted"
/>
</div>
</SplitterPanel>
<!-- 右:预览(不指定默认宽,自动占剩余空间) -->
<SplitterPanel :min="440">
<FilePreviewPanel
:file="selectedFile"
:file-list="currentFileList"
@prev="onPreviewPrev"
@next="onPreviewNext"
/>
</SplitterPanel>
</Splitter>
</template>
</div>
</template>
<style scoped lang="scss">
.ff-container {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
border: 1px solid var(--fms-border);
border-radius: 6px;
background: var(--fms-card);
overflow: hidden;
}
.ff-splitter {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
/* 分隔条:宽 0 + 无负 margin,不占 flex 宽度,面板在交界处无缝相接,
消除分割线左右两侧的空白条(对比:DefaultLayout 用负 margin 让面板互相
覆盖,但附件三栏背景色不同——白列表 / 灰预览——覆盖会露底,这里改用
::after 扩展命中区,不挤压面板也不遮挡相邻面板背景)。 */
.ff-splitter :deep(> .fms-splitter-bar) {
width: 0;
position: relative;
z-index: 1;
}
/* 命中区:从宽 0 的分隔条向两侧各伸出 3px(共 6px,与 --fms-splitter-trigger-size
一致),保持拖拽热区不变;伪元素的事件命中会冒泡到 .fms-splitter-bar 本体,
pointerdown / 键盘聚焦仍由原逻辑处理 */
.ff-splitter :deep(> .fms-splitter-bar::after) {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: var(--fms-splitter-trigger-size);
cursor: col-resize;
}
.ff-center {
display: flex;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.ff-status {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
gap: 8px;
font-size: 13px;
color: var(--fms-text-secondary);
&--error {
color: var(--fms-danger);
}
}
</style>
|