Compare commits

...
4 Commits
Author SHA1 Message Date
wc24 0b75f8c6f2 fix(filter): 用户搜索/改筛选时清掉 relation 跳转的隐形 _id pin
跳转用 ?filter={_id} 锁单条,但 _id 不在筛选抽屉里、且抽屉 confirm 会把它原样带回,
导致用户再搜索/筛选时被看不见的 _id 一直 AND 着、结果莫名为空。
onSearch + handleFilterConfirm 现在都剥掉 _id(重置本就清空)。
2026-06-29 20:54:19 +08:00
wc24 417318921c fix(menu): 菜单高亮用 $route.path 而非 fullPath
default-active 原绑 $route.fullPath(含 query),地址栏带 ?filter=… 时与菜单项
index('/'+name,纯路径)对不上 → 菜单不高亮/不跟随切换。改用 $route.path。
layout(桌面) + sideM(移动) 两处同改。
2026-06-29 20:48:58 +08:00
wc24 f7ef29a928 feat(relation): 显示名可点 → 跳目标模块并过滤到该 _id(单行)
- VueAdmin: 记录 moduleKey→IRouteInfo(moduleRoutes),加 routeForModule(判已注册+非子模块)
- FieldRelation.display: 目标可达时名字渲成链接,点击 router.push 到目标模块
  + ?filter={_id},落成过滤到单条的列表(框架无详情页,单项查看即此形态)
- 子模块目标/无权限目标不可点(routeForModule 返回 undefined)
2026-06-29 20:38:21 +08:00
wc24 f7ee862e0f feat(relation): 显示改查 refs 字典(替代逐行 _label)
配合 shrimp-server v0.6.7:list 响应带回 refs。
- ShrimpTable: 取 res.refs 并 provide('shrimpRefs')
- FieldRelation.display: inject refs,按 id 查名(单/多选),去掉 displayLabel 依赖
- ShrimpTable displayLabel 绑定去掉 relation(tab/textarea 仍走 _label)
relation 顶层显示行为不变,只是换了供给方式。
2026-06-29 20:19:56 +08:00
6 changed files with 73 additions and 10 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "shrimp-admin", "name": "shrimp-admin",
"version": "0.3.1", "version": "0.3.5",
"private": true, "private": true,
"description": "Shrimp 框架前端 — 消费 ShrimpServer + ShrimpTable 契约的经典 Vue3 + Element Plus 实现", "description": "Shrimp 框架前端 — 消费 ShrimpServer + ShrimpTable 契约的经典 Vue3 + Element Plus 实现",
"main": "src/index.ts", "main": "src/index.ts",
+9
View File
@@ -123,11 +123,19 @@ export class VueAdmin extends ShrimpClient {
userName = '' userName = ''
config: ShrimpAdminConfig config: ShrimpAdminConfig
langList: { lang: string; name: string }[] langList: { lang: string; name: string }[]
/** 已注册成功的模块路由(moduleKey → IRouteInfo),供 relation 跳转/单项查看解析路由 */
moduleRoutes = new Map<string, IRouteInfo>()
getFieldComponent(_moduleKey: string, type: string, _fieldKey?: string): Component { getFieldComponent(_moduleKey: string, type: string, _fieldKey?: string): Component {
return fieldRegistry.get(type) || fieldRegistry.get('string')! return fieldRegistry.get(type) || fieldRegistry.get('string')!
} }
/** 目标模块能否作为「单项查看」跳转目标:已按权限注册路由 且 非子模块。返回其路由信息或 undefined(不可跳) */
routeForModule(moduleKey: string): IRouteInfo | undefined {
if (this.moduleInfo.find(m => m.moduleKey === moduleKey)?.parent) return undefined
return this.moduleRoutes.get(moduleKey)
}
state = reactive({ isMobile: false }) state = reactive({ isMobile: false })
private _token: string | null = null private _token: string | null = null
@@ -256,6 +264,7 @@ export class VueAdmin extends ShrimpClient {
props: { moduleKey: info.moduleKey, events: info.events }, props: { moduleKey: info.moduleKey, events: info.events },
meta: { title: info.label } meta: { title: info.label }
}) })
this.moduleRoutes.set(info.moduleKey, info)
} }
initRouter() { initRouter() {
+18 -3
View File
@@ -29,7 +29,7 @@
<slot :name="`column-${field.key}`" :row="row" :field="field"> <slot :name="`column-${field.key}`" :row="row" :field="field">
<component :is="vueAdmin.getFieldComponent(moduleKey, field.type, field.key)" :field="field" <component :is="vueAdmin.getFieldComponent(moduleKey, field.type, field.key)" :field="field"
:model-value="row[field.key]" mode="display" :model-value="row[field.key]" mode="display"
v-bind="field.type === 'relation' || field.type === 'textarea' || field.type === 'tab' ? { displayLabel: row[field.key + '_label'] } : {}" /> v-bind="field.type === 'textarea' || field.type === 'tab' ? { displayLabel: row[field.key + '_label'] } : {}" />
</slot> </slot>
</template> </template>
</el-table-column> </el-table-column>
@@ -78,7 +78,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, useSlots, onMounted, type Component } from 'vue' import { ref, computed, useSlots, onMounted, provide, type Component } from 'vue'
import { ITableCrudProto, IFieldConfig, IModuleInfo, IPageQuery } from '../IShrimpType' import { ITableCrudProto, IFieldConfig, IModuleInfo, IPageQuery } from '../IShrimpType'
import { vueAdmin } from '../VueAdmin' import { vueAdmin } from '../VueAdmin'
import { Plus, Delete as DeleteIcon, Download, Upload, Edit, VideoPlay, VideoPause, Operation } from '@element-plus/icons-vue' import { Plus, Delete as DeleteIcon, Download, Upload, Edit, VideoPlay, VideoPause, Operation } from '@element-plus/icons-vue'
@@ -132,8 +132,18 @@ const {
const searchVisible = ref(false) const searchVisible = ref(false)
// relation 跳转用 ?filter={_id} 锁定单条;_id 是隐形 pin(不在筛选抽屉里),
// 用户一旦搜索/改筛选就清掉它,避免被看不见的 _id 一直 AND 着导致结果莫名为空
function stripJumpPin() {
if ('_id' in filterData.value) {
const { _id, ...rest } = filterData.value
filterData.value = rest
}
}
function onSearch() { function onSearch() {
searchKeyword.value = keyword.value searchKeyword.value = keyword.value
stripJumpPin()
currentPage.value = 1 currentPage.value = 1
refresh() refresh()
} }
@@ -151,7 +161,8 @@ const hasFilter = computed(() => {
}) })
async function handleFilterConfirm(newFilterData: Record<string, any>) { async function handleFilterConfirm(newFilterData: Record<string, any>) {
filterData.value = newFilterData const { _id, ...rest } = newFilterData // 确认筛选 → 丢弃跳转 pin
filterData.value = rest
currentPage.value = 1 currentPage.value = 1
await refresh() await refresh()
} }
@@ -214,6 +225,9 @@ const filterSlotNames = computed(() => Object.keys(slots).filter(name => name.st
const data = ref<any[]>([]) const data = ref<any[]>([])
const loading = ref(false) const loading = ref(false)
const total = ref(0) const total = ref(0)
// relation 显示名字典 { moduleKey: { id: 名 } },由 list 响应带回,provide 给表格里的字段组件查名
const refs = ref<Record<string, Record<string, any>>>({})
provide('shrimpRefs', refs)
// ---- 对话框状态 ---- // ---- 对话框状态 ----
@@ -501,6 +515,7 @@ async function refresh(query?: Record<string, any>) {
Object.assign(pageQuery, query) Object.assign(pageQuery, query)
const res = await api.list(pageQuery) const res = await api.list(pageQuery)
data.value = res.list || [] data.value = res.list || []
refs.value = res.refs || {}
total.value = res.total total.value = res.total
currentPage.value = res.page currentPage.value = res.page
currentPageSize.value = res.pageSize currentPageSize.value = res.pageSize
+43 -4
View File
@@ -1,7 +1,13 @@
<template> <template>
<template v-if="mode === 'display'"> <template v-if="mode === 'display'">
<template v-if="field.multiple && Array.isArray(displayLabel)">{{ displayLabel.filter(Boolean).join(', ') || '-' }}</template> <template v-if="field.multiple">
<template v-else>{{ displayLabel || modelValue || '-' }}</template> <span v-if="!displayItems.length">-</span>
<template v-for="(it, i) in displayItems" :key="i"><a v-if="jumpable" class="rel-link" @click.stop="goto(it.id)">{{ it.name }}</a><span v-else>{{ it.name }}</span><span v-if="i < displayItems.length - 1">、</span></template>
</template>
<template v-else>
<a v-if="jumpable && modelValue != null" class="rel-link" @click.stop="goto(modelValue)">{{ displayName }}</a>
<span v-else>{{ displayName || '-' }}</span>
</template>
</template> </template>
<el-select <el-select
v-else-if="mode === 'filter'" v-else-if="mode === 'filter'"
@@ -18,7 +24,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, computed } from 'vue' import { ref, inject, onMounted, computed, type Ref } from 'vue'
import { vueAdmin } from '../../VueAdmin' import { vueAdmin } from '../../VueAdmin'
import { ITableCrudProto } from '../../IShrimpType' import { ITableCrudProto } from '../../IShrimpType'
@@ -27,7 +33,6 @@ const props = defineProps<{
modelValue: any modelValue: any
mode: 'display' | 'edit' | 'filter' mode: 'display' | 'edit' | 'filter'
disabled?: boolean disabled?: boolean
displayLabel?: string | string[]
}>() }>()
defineEmits<{ defineEmits<{
@@ -37,6 +42,30 @@ defineEmits<{
const options = ref<{ label: string; value: string }[]>([]) const options = ref<{ label: string; value: string }[]>([])
const loading = ref(false) const loading = ref(false)
// 显示用:从 ShrimpTable provide 的 refs 字典按 id 查名(替代旧的 _label)
const refs = inject<Ref<Record<string, Record<string, any>>>>('shrimpRefs', ref<Record<string, Record<string, any>>>({}))
const dict = computed(() => refs.value?.[props.field.relation?.moduleKey] ?? {})
const displayName = computed(() => {
const id = props.modelValue
if (id == null) return ''
return dict.value[String(id)] ?? String(id)
})
const displayItems = computed(() =>
(Array.isArray(props.modelValue) ? props.modelValue : [])
.filter((id: any) => id != null)
.map((id: any) => ({ id, name: dict.value[String(id)] ?? String(id) }))
)
// 单项查看跳转:目标模块可达(已注册路由)且非子模块时,名字可点 → 跳目标模块并过滤到该 _id(单行)
const jumpable = computed(() =>
props.mode === 'display' && !!props.field.relation && !!vueAdmin.routeForModule(props.field.relation.moduleKey)
)
function goto(id: any) {
const r = props.field.relation && vueAdmin.routeForModule(props.field.relation.moduleKey)
if (!r || id == null) return
vueAdmin.router.push({ name: r.name, query: { filter: JSON.stringify({ _id: String(id) }) } })
}
/** 统一转为字符串值,解决 ObjectId 与 el-select 的匹配问题 */ /** 统一转为字符串值,解决 ObjectId 与 el-select 的匹配问题 */
const stringValue = computed(() => { const stringValue = computed(() => {
// filter 模式强制 multiple,始终返回数组 // filter 模式强制 multiple,始终返回数组
@@ -92,3 +121,13 @@ onMounted(async () => {
} }
}) })
</script> </script>
<style scoped>
.rel-link {
color: var(--el-color-primary);
cursor: pointer;
}
.rel-link:hover {
text-decoration: underline;
}
</style>
+1 -1
View File
@@ -24,7 +24,7 @@
<div v-if="!vueAdmin.state.isMobile && nextMenu.length > 0" class="aminui-side"> <div v-if="!vueAdmin.state.isMobile && nextMenu.length > 0" class="aminui-side">
<div class="adminui-side-scroll"> <div class="adminui-side-scroll">
<el-scrollbar> <el-scrollbar>
<el-menu :default-active="$route.fullPath" router> <el-menu :default-active="$route.path" router>
<navMenu :navMenus="nextMenu"></navMenu> <navMenu :navMenus="nextMenu"></navMenu>
</el-menu> </el-menu>
</el-scrollbar> </el-scrollbar>
+1 -1
View File
@@ -17,7 +17,7 @@
<el-main> <el-main>
<el-scrollbar> <el-scrollbar>
<el-menu <el-menu
:default-active="$route.fullPath" :default-active="$route.path"
@select="select" @select="select"
router router
background-color="#212d3d" background-color="#212d3d"