refactor(spec): 按《云海子应用前端接入规范》v1.10 全量对齐(回收旧皮肤+高度实测模型+工具条/操作列收口)
build / build (push) Successful in 2m50s
build / build (push) Successful in 2m50s
回收(§2.2):删本地旧皮肤 layout.css/icons.css/icons.css.bak 及 main.ts 两处 import——消除「接了通道却被本地旧皮肤同优先级盖住」的根因;layout.vue 同批三改:删顶栏房子图标钮、面包屑改 -0.125em 基线流 + icon-category→icon-shouye、getMenuIcon 改读 item.icon 兜底 icon-list。
表格高度(§4.3/§4.4):引入 useElementHeight.ts(与 UserBase 逐字节一致);TablePageAlovaNew 换三端同构版(.table-region + 实测高);4 个列表页清 useTableHeight/ResizeObserver(document.body)/:tableHeight,根节点补 yh-fill;无分页两页不套组件,各包 .table-region + 独立实测。
工具条/tab/操作列(§4.1/§4.2/§5/§8.1):去 size="large"×5;ServiceFormManage 工具条右端说明文案改表格上方常驻 Alert(type=info/show-icon/无 closable);ServiceItemList「同步部门名」归左;ServiceDictManage 去 Tabs type="card" 改 tabs-wrapper yh-fill > main-container yh-tabs-card > Tabs.tabs-panel + 双 tab-body;引入 tableActions.ts,4 页迁 actionColumn({mode:"icon"}),发布/下架等互斥动作改 disabledWhen 置灰保位;h("Tag") 改真组件 ×5。
收尾(P2):view-ui-plus ^1.3.24(lock/node_modules 同版);删死代码 TablePage.vue/TablePageAlova.vue/TreeNode/index copy.vue;main.scss 去 bscroll import 并删文件。未动 service/**、public/libs/**(ztree 仍在用)与规范文档。
This commit is contained in:
+1
-1
@@ -28,7 +28,7 @@
|
||||
"pdfjs-dist": "^4.10.38",
|
||||
"pinia": "^2.1.4",
|
||||
"sass": "^1.64.1",
|
||||
"view-ui-plus": "^1.3.16",
|
||||
"view-ui-plus": "^1.3.24",
|
||||
"vite-plugin-mock": "2.9.6",
|
||||
"vue": "^3.4.19",
|
||||
"vue-router": "4",
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ body {
|
||||
}
|
||||
/* 页面级滚动容器:main-wrapper 只有 min-height 时内容超一屏会被 body 的
|
||||
overflow:hidden 直接裁掉(如发通知页),无任何滚动条。定高 + 纵向滚动,
|
||||
放得下的列表页(表格高度由 Utils.useTableHeight 算好)不受影响 */
|
||||
列表页表格高度由 useElementHeight 实测 .table-region 得出,不依赖此处滚动 */
|
||||
.base-wrap-main .main-wrapper {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/*滚动条样式 */
|
||||
|
||||
/*第一种样式,所有的滚动条,都默认这个样子*/
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/*正常情况下滑块的样式*/
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, .05);
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 1px 1px 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
/*鼠标悬浮在该类指向的控件上时滑块的样式*/
|
||||
*:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, .2);
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 1px 1px 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
/*鼠标悬浮在滑块上时滑块的样式*/
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, .3);
|
||||
-webkit-box-shadow: inset 1px 1px 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
/*正常时候的主干部分*/
|
||||
*::-webkit-scrollbar-track {
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/*鼠标悬浮在滚动条上的主干部分*/
|
||||
*::-webkit-scrollbar-track:hover {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .4);
|
||||
background-color: rgba(0, 0, 0, .01);
|
||||
}
|
||||
|
||||
/*第一种样式结束*/
|
||||
|
||||
|
||||
/*第二种样式*/
|
||||
|
||||
.scroll2::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/*正常时候的主干部分*/
|
||||
.scroll2::-webkit-scrollbar-track {
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/*正常情况下滑块的样式*/
|
||||
.scroll2::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, .05);
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 1px 1px 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
/*鼠标悬浮在该类指向的控件上时滑块的样式*/
|
||||
.scroll2:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, .2);
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 1px 1px 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
/*鼠标悬浮在滚动条上的主干部分*/
|
||||
.scroll2::-webkit-scrollbar-track:hover {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
/*鼠标悬浮在滑块上时滑块的样式*/
|
||||
.scroll2::-webkit-scrollbar-thumb:hover {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
/*第二种样式结束*/
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,4 @@
|
||||
@import "base/base";
|
||||
@import "base/bscroll.css";
|
||||
@import "base/reset";
|
||||
|
||||
@import "utils/mixins";
|
||||
@@ -115,3 +114,44 @@ h4.subtitle {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* ================= 表格操作列(utils/tableActions.ts)=================
|
||||
.op-cell:flex 横排容器——把 iView Tooltip/Dropdown 的块级包装层降级为 flex item,
|
||||
强制横排(根因:.operating-btn 在通道基线 display:block,.ivu-tooltip 包装层默认 block,
|
||||
逐钮竖排;不要再在 cell 上动 display,会牵连其它列)。
|
||||
先落工程内,四端同构后此段上提 YschTheme 主题层。 */
|
||||
.op-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
|
||||
// 链间距统一由容器 gap 管理,退役通道基线的 padding-right:10px
|
||||
.operating-link {
|
||||
padding-right: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* 「更多▾」触发器(Dropdown transfer 后菜单挂 body,样式必须全局) */
|
||||
.op-more {
|
||||
.op-more-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.op-more-arrow {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 「更多」菜单内危险项(红色 + hover 红底,与 text-error 同源 $color-error) */
|
||||
.ivu-dropdown-item.op-item-danger {
|
||||
color: $color-error !important;
|
||||
|
||||
&:hover {
|
||||
color: $color-error !important;
|
||||
background-color: rgba(232, 104, 74, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { OperatorEnum } from "@/types";
|
||||
import { PageData, PageParams } from "@/types/Basics";
|
||||
import Utils from "@/utils";
|
||||
import { Message } from "view-ui-plus";
|
||||
|
||||
/* 表格插件参数类型 */
|
||||
interface TablePageProps {
|
||||
dataApi: (params: PageParams) => Promise<PageData>;
|
||||
columns: object[];
|
||||
tableHeight: number | undefined;
|
||||
pageParams: PageParams | undefined;
|
||||
dataFilter?: ((resp: object) => object[]) | undefined;
|
||||
autoLoad?: boolean | undefined;
|
||||
}
|
||||
import { ref, onBeforeMount, onMounted } from "vue";
|
||||
const props = withDefaults(defineProps<TablePageProps>(), {
|
||||
autoLoad: true,
|
||||
});
|
||||
const list = ref<any[]>([]),
|
||||
selectList = ref<number[]>([]),
|
||||
total = ref(0),
|
||||
tableH = ref(0),
|
||||
tableLoading = ref(true);
|
||||
const params = ref<PageParams>({ page: 1, count: 10 });
|
||||
onBeforeMount(async () => {
|
||||
if (props.tableHeight) tableH.value = props.tableHeight;
|
||||
else tableH.value = Utils.useTableHeight();
|
||||
if (!!props.pageParams) params.value = props.pageParams;
|
||||
// 页长记忆(bsch 规范 §3:customPageCount localStorage)
|
||||
params.value.count = parseInt(localStorage.getItem("customPageCount") || String(params.value.count));
|
||||
if (props.autoLoad) await getData();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// window.onresize = _.debounce(() => {
|
||||
// tableH.value = Utils.useTableHeight();
|
||||
// }, 1000);
|
||||
});
|
||||
|
||||
const getData = async () => {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
const tempParams: any = _.cloneDeep(params.value);
|
||||
tempParams.size = tempParams.count;
|
||||
delete tempParams.count;
|
||||
|
||||
tempParams.page -= 1;
|
||||
|
||||
var data = await props.dataApi(tempParams);
|
||||
total.value = data.count;
|
||||
|
||||
if (typeof props.dataFilter != "undefined") list.value = props.dataFilter(data);
|
||||
else list.value = data.list;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
Message.error(error);
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
function selectChange(selection: any) {
|
||||
selectList.value = selection;
|
||||
}
|
||||
async function pageUpdate(page: number) {
|
||||
params.value.page = page;
|
||||
await getData();
|
||||
}
|
||||
async function pageSizeUpdate(size: number) {
|
||||
params.value.page = 1;
|
||||
params.value.count = size;
|
||||
localStorage.setItem("customPageCount", String(size));
|
||||
await getData();
|
||||
}
|
||||
|
||||
/** 刷新数据-更新删除用 */
|
||||
// "add" | "del" | "edit" | "sel"
|
||||
async function refreshData() {
|
||||
params.value.page = 1;
|
||||
await getData();
|
||||
}
|
||||
|
||||
/** 重置数据 */
|
||||
// "add" | "del" | "edit" | "sel"
|
||||
async function reloadData(type: OperatorEnum = OperatorEnum.sel) {
|
||||
let { page, count } = params.value;
|
||||
if (type == OperatorEnum.add) {
|
||||
console.log(page, count, total.value);
|
||||
|
||||
if (page * count == total.value) page += 1;
|
||||
} else if (type == OperatorEnum.del) {
|
||||
if ((page - 1) * count + 1 == total.value) page -= 1;
|
||||
if (page < 1) page = 1;
|
||||
} else if (type == OperatorEnum.edit) {
|
||||
//page=page
|
||||
} else if (type == OperatorEnum.sel) page = 1;
|
||||
params.value.page = page;
|
||||
await getData();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
list,
|
||||
refreshData,
|
||||
reloadData,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data="list"
|
||||
@on-selection-change="selectChange"
|
||||
class="link-table table-bottom-box"
|
||||
:height="tableH"
|
||||
:loading="tableLoading"
|
||||
no-data-text="暂无数据"
|
||||
>
|
||||
<template #loading>
|
||||
<div class="table-spin-load">加载中</div>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="page-panel">
|
||||
<div class="fl ml-15 align-middle">
|
||||
<slot :checkList="selectList"></slot>
|
||||
</div>
|
||||
|
||||
<Page
|
||||
class="page-box fr"
|
||||
:total="total"
|
||||
:model-value="params.page"
|
||||
:page-size="params.count"
|
||||
:page-size-opts="[10, 20, 30, 40]"
|
||||
@on-page-size-change="pageSizeUpdate"
|
||||
@on-change="pageUpdate"
|
||||
placement="top"
|
||||
show-total
|
||||
show-sizer
|
||||
></Page>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style></style>
|
||||
@@ -1,119 +0,0 @@
|
||||
<template>
|
||||
<!-- 表格 -->
|
||||
<Table :columns="columns" :data="list" :height="tableH" :loading="loading" no-data-text="暂无数据"
|
||||
class="link-table table-bottom-box" @on-selection-change="selectChange">
|
||||
<template #loading>
|
||||
<div class="table-spin-load">加载中</div>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<!-- 分页 / 插槽 -->
|
||||
<div class="page-panel">
|
||||
<div class="fl ml-15 align-middle">
|
||||
<slot :checkList="selectList" />
|
||||
</div>
|
||||
|
||||
<Page class="page-box fr" :total="total" :model-value="page" :page-size="pageSize"
|
||||
:page-size-opts="[10, 20, 30, 40]" show-total show-sizer placement="top" @on-change="onPageChange"
|
||||
@on-page-size-change="onSizeChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { Message } from 'view-ui-plus'
|
||||
import Utils from '@/utils'
|
||||
import { OperatorEnum } from '@/types'
|
||||
|
||||
interface Props {
|
||||
/** 必填:alova-gen 生成的 selectTextbook 方法(或任意同形方法) */
|
||||
dataApi: any
|
||||
columns: any[]
|
||||
dataParams: any
|
||||
tableHeight?: number
|
||||
autoLoad?: boolean
|
||||
dataFilter?: (resp: any) => any[],
|
||||
formatResponse?: (res: any) => { total: number; list: any[] }
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
autoLoad: true,
|
||||
formatResponse: (res: any) => ({ total: res.total, list: res.content })
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'selection-change', list: number[]): void
|
||||
}>()
|
||||
|
||||
onMounted(() => {
|
||||
})
|
||||
|
||||
/* ---------------- 表格高度 ---------------- */
|
||||
const tableH = ref(props.tableHeight ?? Utils.useTableHeight())
|
||||
|
||||
/* ---------------- 选中行 ---------------- */
|
||||
const selectList = ref<number[]>([])
|
||||
function selectChange(rows: any[]) {
|
||||
selectList.value = rows.map((r: any) => r.id)
|
||||
emit('selection-change', selectList.value)
|
||||
}
|
||||
|
||||
/* ---------------- 分页 + 请求 ---------------- */
|
||||
const {
|
||||
loading,
|
||||
data: resp,
|
||||
send: sendLoad,
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
reload
|
||||
} = usePagination(
|
||||
(current: number, size: number) =>
|
||||
props.dataApi({
|
||||
...props.dataParams, // 合并外部条件
|
||||
pageIndex: current,
|
||||
pageSize: size,
|
||||
}),
|
||||
{
|
||||
// total: res => res.total,
|
||||
// data: res => res.list,
|
||||
total: res => props.formatResponse(res).total,
|
||||
data: res => props.formatResponse(res).list,
|
||||
// 默认分页参数
|
||||
initialPage: 1,
|
||||
initialPageSize: 10,
|
||||
immediate: props.autoLoad,
|
||||
watchingStates: [() => props.dataParams]
|
||||
}
|
||||
)
|
||||
/* 列表数据 */
|
||||
const list = computed(() => {
|
||||
if (!resp.value) return []
|
||||
return props.dataFilter ? props.dataFilter(resp.value) : resp.value
|
||||
})
|
||||
|
||||
/* ---------------- 翻页事件 ---------------- */
|
||||
const onPageChange = (p: number) => (page.value = p)
|
||||
const onSizeChange = (s: number) => (pageSize.value = s)
|
||||
|
||||
/* ---------------- 外部可调用 ---------------- */
|
||||
function refreshData() {
|
||||
page.value = 1
|
||||
reload()
|
||||
}
|
||||
function reloadData(type: OperatorEnum = OperatorEnum.sel) {
|
||||
let p = page.value
|
||||
const s = pageSize.value
|
||||
|
||||
if (type === OperatorEnum.add && p * s === total.value) p += 1
|
||||
if (type === OperatorEnum.del && (p - 1) * s + 1 === total.value) p -= 1
|
||||
if (p < 1) p = 1
|
||||
page.value = p
|
||||
sendLoad()
|
||||
}
|
||||
|
||||
defineExpose({ list, refreshData, reloadData })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 沿用旧样式即可 */
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<div class="table-region" ref="regionRef">
|
||||
<Table :columns="columns" :data="list" :height="tableH" :loading="loading" no-data-text="暂无数据"
|
||||
class="link-table table-bottom-box" @on-selection-change="selectChange">
|
||||
<template #loading>
|
||||
@@ -8,6 +9,7 @@
|
||||
<slot :name="slotName" v-bind="slotProps" />
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div class="page-panel">
|
||||
<div class="fl ml-15 align-middle">
|
||||
@@ -22,7 +24,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useSlots, watch } from 'vue'
|
||||
import Utils from '@/utils'
|
||||
import { useElementHeight } from '@/utils/hook/useElementHeight'
|
||||
import { OperatorEnum } from '@/types'
|
||||
|
||||
interface Props {
|
||||
@@ -44,11 +46,10 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
const slots = useSlots()
|
||||
|
||||
const tableH = ref(props.tableHeight ?? Utils.useTableHeight())
|
||||
// 页面侧挂载后可能重算高度(嵌入面板初始视口先小后大),跟随 props 更新
|
||||
watch(() => props.tableHeight, v => {
|
||||
if (v) tableH.value = v
|
||||
})
|
||||
const regionRef = ref<HTMLElement | null>(null)
|
||||
const measuredH = useElementHeight(regionRef)
|
||||
// 显式传 tableHeight 则尊重之;否则实测 .table-region(flex:1)高度喂固定表头,首帧未测出前用 undefined(auto)
|
||||
const tableH = computed(() => props.tableHeight ?? (measuredH.value || undefined))
|
||||
const tableSlotNames = computed(() => Object.keys(slots).filter(name => !['default', 'loading'].includes(name)))
|
||||
|
||||
const selectList = ref<number[]>([])
|
||||
|
||||
@@ -1,487 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="opera-ul">
|
||||
<Tooltip content="新增节点" placement="top" transfer>
|
||||
<li @click="showAddNode" :class="{ disabled: !nodePermissions.canCreate }">
|
||||
<SvgIcon name="tree-icon-1" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
<Tooltip content="编辑节点" placement="top" transfer>
|
||||
<li @click="showEditNode" :class="{ disabled: !nodePermissions.canUpdate }">
|
||||
<SvgIcon name="tree-icon-2" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
<Tooltip content="移动节点" placement="top" transfer>
|
||||
<li @click="showMoveNode" :class="{ disabled: !nodePermissions.canMove }">
|
||||
<SvgIcon name="tree-icon-3" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="删除节点" placement="top" transfer>
|
||||
<li @click="showDeleteNode" :class="{ disabled: !nodePermissions.canDelete }">
|
||||
<SvgIcon name="tree-icon-4" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
</ul>
|
||||
<Select class="mt-8" v-model="curSubJectId" placeholder="请选择学科">
|
||||
<Option v-for="item in subjectComp" :value="item.k">{{ item.v }}</Option>
|
||||
</Select>
|
||||
<MyTree v-if="!treeLoading" ref="refMyTree" :autoClick="false" :treeData="treeData" :setting="treeSetting"
|
||||
@on-tree-click="treeLink" />
|
||||
|
||||
<div class="modal-wrap">
|
||||
<Modal v-model="editNodeModal.modal" :title="editNodeModal.title" width="450px" :mask-closable="false">
|
||||
<Form ref="refFormData" :label-width="100" :model="editNodeModal" class="myform mb-20 mr-20">
|
||||
<Form-Item label="当前位置:">
|
||||
{{ curNodePath.join(' > ') }}
|
||||
</Form-Item>
|
||||
<Form-Item label="节点名称:" prop="name"
|
||||
:rules="{ required: true, message: '请输入节点名称', trigger: 'blur' }">
|
||||
<Input ref="refNodeNameInput" maxlength="30" v-model="editNodeModal.name" />
|
||||
</Form-Item>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button @click="editNodeModal.modal = false">取消</Button>
|
||||
<Button type="primary" :loading="editNodeModal.loading" @click="saveEditNode">确认</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<Modal id="modal-sortTreeNode-id" class-name="up-dowm-components" v-model="showMoveModalStatus" title="移动节点"
|
||||
:closable="false" :mask-closable="false" width="600">
|
||||
<div class="fix">
|
||||
<Alert class="fl">拖拽到目标节点时,确保节点是展开状态,以保证节点数据一致</Alert>
|
||||
<Button class="fr" @click="sendReloadTree()">刷新节点树</Button>
|
||||
</div>
|
||||
<MyTree class="tree-div" ref="refSortMyTree" :autoClick="false" :treeData="moveTreeData"
|
||||
:setting="moveTreeSetting" @on-tree-drag="onTreeDragMove" />
|
||||
|
||||
<template #footer>
|
||||
<Button @click="showMoveModalStatus = false" style="margin-left: 20px">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import MyTree from '@/components/MyTree.vue'
|
||||
import { useDefaultStore } from "@/store";
|
||||
import { RootNode } from "@/types";
|
||||
import { cloneDeep, isEmpty } from "lodash";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
import { get, last } from "lodash";
|
||||
|
||||
const props = defineProps<{
|
||||
rootType: RootNode;
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'on-change': [stage: number, subject: string, treeNode: any]
|
||||
}>()
|
||||
|
||||
|
||||
const curTreeNode = defineModel<any>('curTreeNode', { default: {} })
|
||||
//学科学段
|
||||
const curSubjectInfo = defineModel<any>('curSubjectInfo', {
|
||||
default: {
|
||||
stage: null,
|
||||
subject: null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const store = useDefaultStore();
|
||||
const { $utils } = useCurrentInstance();
|
||||
|
||||
const refMyTree = useTemplateRef('refMyTree');
|
||||
const refFormData = useTemplateRef('refFormData')
|
||||
const refNodeNameInput = useTemplateRef('refNodeNameInput')
|
||||
|
||||
|
||||
const treeSetting = ref({
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showIcon: false,
|
||||
},
|
||||
edit: {
|
||||
enable: true,
|
||||
showRemoveBtn: false,
|
||||
showRenameBtn: false,
|
||||
drag: false,
|
||||
},
|
||||
check: {
|
||||
enable: false,
|
||||
},
|
||||
callback: {},
|
||||
})
|
||||
|
||||
const moveTreeSetting = ref({
|
||||
view: { dblClickExpand: false, showIcon: true },
|
||||
edit: {
|
||||
enable: true,
|
||||
showRemoveBtn: false,
|
||||
showRenameBtn: false,
|
||||
drag: {
|
||||
isMove: true,
|
||||
isCopy: false,
|
||||
showArrow: true,
|
||||
prev: true,
|
||||
next: true,
|
||||
inner: true,
|
||||
autoExpandTrigger: true
|
||||
}
|
||||
},
|
||||
check: {
|
||||
enable: false
|
||||
},
|
||||
callback: {}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
const showMoveModalStatus = ref(false)
|
||||
const editNodeModal = reactive({
|
||||
modal: false,
|
||||
title: '',
|
||||
loading: false,
|
||||
name: null,
|
||||
id: null,
|
||||
})
|
||||
|
||||
|
||||
const curSubJectId = ref<number | null>(null);
|
||||
|
||||
const subjectComp = computed(() => {
|
||||
return store.getStageSubjects;
|
||||
})
|
||||
|
||||
const paramsComp = computed(() => {
|
||||
const find = subjectComp.value.find((item: any) => item.k == curSubJectId.value)
|
||||
if (!find) return null;
|
||||
else
|
||||
return {
|
||||
rootType: props.rootType,
|
||||
stage: find.stage,
|
||||
subject: find.code
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const curNodePath = computed(() => {
|
||||
if (!curTreeNode.value) return [];
|
||||
let arr: any[] = curTreeNode.value.getPath().map((x: any) => x.name)
|
||||
return arr;
|
||||
})
|
||||
|
||||
const moveTreeData = computed(() => {
|
||||
function formatTree(list: any[]): any[] {
|
||||
return list
|
||||
.map(node => {
|
||||
const [, , canMove] = $utils.unpackPerms(node.permissions)
|
||||
if (!canMove) return null
|
||||
|
||||
const copy = cloneDeep(node)
|
||||
copy.drag = true
|
||||
copy.children = copy.children?.length
|
||||
? formatTree(copy.children)
|
||||
: null
|
||||
return copy
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
return formatTree(treeData.value)
|
||||
})
|
||||
|
||||
const { loading: treeLoading, data: treeData, send: sendReloadTree, onSuccess: onNodeTreeSuccess } = useRequest(
|
||||
() => Apis.NodeTree.getNodeTree({ params: paramsComp.value }),
|
||||
{
|
||||
initialData: [],
|
||||
immediate: false,
|
||||
})
|
||||
|
||||
watch(() => curSubJectId.value, async (val) => {
|
||||
await nextTick();
|
||||
curTreeNode.value = null;
|
||||
curSubjectInfo.value.stage = paramsComp.value?.stage;
|
||||
curSubjectInfo.value.subject = paramsComp.value?.subject;
|
||||
if (!!val)
|
||||
sessionStorage.setItem('bsch.learnmanage.cur_subject_id', val.toString())
|
||||
sendReloadTree(-1);
|
||||
})
|
||||
|
||||
onNodeTreeSuccess(() => reloadTree(-1))
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
|
||||
if (subjectComp.value.length > 0) {
|
||||
|
||||
let cacheStr = sessionStorage.getItem('bsch.learnmanage.cur_subject_id');
|
||||
if (!!cacheStr) {
|
||||
curSubJectId.value = Number(cacheStr);
|
||||
}
|
||||
else
|
||||
curSubJectId.value = subjectComp.value[0].k;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
async function reloadTree(id?: number) {
|
||||
await nextTick();
|
||||
console.log(id);
|
||||
|
||||
treeData.value.forEach((n: any) => (n.open = true));
|
||||
if (!!curTreeNode.value && !!curTreeNode.value.id)
|
||||
refMyTree.value?.reloadTree(curTreeNode.value.id)
|
||||
else
|
||||
refMyTree.value?.reloadTree(id)
|
||||
}
|
||||
|
||||
|
||||
function showAddNode() {
|
||||
if (!nodePermissions.value.canCreate) return;
|
||||
editNodeModal.title = '新增节点';
|
||||
editNodeModal.modal = true;
|
||||
editNodeModal.id = null;
|
||||
editNodeModal.name = null;
|
||||
nextTick().then(() => {
|
||||
refNodeNameInput.value?.focus()
|
||||
})
|
||||
}
|
||||
function showEditNode() {
|
||||
if (!nodePermissions.value.canUpdate) return;
|
||||
editNodeModal.title = '编辑节点';
|
||||
editNodeModal.modal = true;
|
||||
|
||||
editNodeModal.id = curTreeNode.value.id;
|
||||
editNodeModal.name = curTreeNode.value.name;
|
||||
nextTick().then(() => {
|
||||
refNodeNameInput.value.focus()
|
||||
})
|
||||
}
|
||||
function showMoveNode() {
|
||||
if (!nodePermissions.value.canMove) return;
|
||||
showMoveModalStatus.value = true;
|
||||
|
||||
}
|
||||
async function showDeleteNode() {
|
||||
if (!nodePermissions.value.canDelete) return;
|
||||
const temp = cloneDeep(curTreeNode.value);
|
||||
const parentNode = temp.getParentNode();
|
||||
Modal.confirm({
|
||||
title: '删除知识树节点',
|
||||
content: `确定删除名为【${curTreeNode.value.name}】的知识树节点记录?`,
|
||||
onOk: async () => {
|
||||
const id = curTreeNode.value.id;
|
||||
await Apis.NodeTree.deleteNode({ params: { nodeId: id } })
|
||||
Message.success('删除成功!');
|
||||
curTreeNode.value = parentNode;
|
||||
sendReloadTree()
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async function saveEditNode() {
|
||||
let isAdd = !!editNodeModal.id ? false : true;
|
||||
const validate = await refFormData.value.validate();
|
||||
if (!validate) {
|
||||
Message.error('请填写红色星号内容!');
|
||||
return;
|
||||
}
|
||||
const { id, name } = editNodeModal;
|
||||
editNodeModal.loading = true;
|
||||
if (isAdd) {
|
||||
const parentId = get(curTreeNode.value, 'id', -1)
|
||||
const brotherId = get(last(get(curTreeNode.value, 'children')), 'id', 0);
|
||||
const params: any = {
|
||||
rootType: props.rootType,
|
||||
name, parentId, brotherId,
|
||||
stage: paramsComp.value?.stage,
|
||||
subject: paramsComp.value?.subject
|
||||
};
|
||||
await Apis.NodeTree.addNode({ params })
|
||||
sendReloadTree()
|
||||
}
|
||||
else {
|
||||
await Apis.NodeTree.updateNodeName({ params: { name, nodeId: id } })
|
||||
|
||||
sendReloadTree();
|
||||
}
|
||||
|
||||
editNodeModal.loading = false;
|
||||
editNodeModal.modal = false;
|
||||
}
|
||||
|
||||
async function refreshMoveTree() {
|
||||
// const id = selectedNode.value.id;
|
||||
// await Apis.NodeTree.refreshSortTree({ params: { nodeId: id } })
|
||||
// Message.success('刷新成功!');
|
||||
// refTreeNode.value?.reloadTree()
|
||||
}
|
||||
//@ts-ignore
|
||||
async function onTreeDragMove({ treeNodes, targetNode, moveType }) {
|
||||
let mType = moveType == "inner" ? 3 : moveType == "prev" ? 1 : moveType == "next" ? 2 : 0;
|
||||
let currentNode = treeNodes[0];
|
||||
let nativeNode = currentNode;
|
||||
const params = formatMoveTree(treeNodes[0], targetNode, mType);
|
||||
await Apis.NodeTree.move({
|
||||
params: params,
|
||||
})
|
||||
sendReloadTree();
|
||||
}
|
||||
|
||||
function formatMoveTree(currentNode: any, targetNode: any, moveType: any) {
|
||||
|
||||
let nativeNode = currentNode;
|
||||
let treeNode = targetNode;
|
||||
let pathArr = treeNode.path.split(",");
|
||||
let index2 = pathArr.findIndex((ele: any) => {
|
||||
return ele == nativeNode.id;
|
||||
});
|
||||
if (index2 != -1) {
|
||||
Message.warning("父节点无法移动到子节点下方");
|
||||
return;
|
||||
}
|
||||
let newBrother = ""; //新兄长节点 【接口参数】
|
||||
let newParent = ""; //新父节点 【接口参数】
|
||||
|
||||
console.log('treeNode', treeNode);
|
||||
|
||||
var parentNode = treeNode.getParentNode();
|
||||
|
||||
let chilList = !!parentNode ? parentNode.children : treeNode.children;
|
||||
let index = chilList.findIndex((ele: any) => {
|
||||
return ele.id == treeNode.id;
|
||||
});
|
||||
if (treeNode.id == nativeNode.id || (treeNode.id == nativeNode.parentId && moveType == 3)) {
|
||||
//选中同一节点,无效移动
|
||||
Message.warning("目标节点与选中节点冲突,请选择其他目标节点");
|
||||
return;
|
||||
} else if ((moveType == 1 || moveType == 2) && nativeNode.parentId == treeNode.parentId) {
|
||||
if (moveType == 1) {
|
||||
console.log('idnex:' + index);
|
||||
index--;
|
||||
//前面
|
||||
newBrother = index == 0 ? 0 : chilList[index - 1].id;
|
||||
} else {
|
||||
//后面
|
||||
newBrother = chilList[index].id;
|
||||
}
|
||||
newParent = "";
|
||||
} else {
|
||||
//跨父节点移动
|
||||
if (moveType == 1) {
|
||||
//选择的是前面
|
||||
//前面
|
||||
newBrother = index == 0 ? index : chilList[index - 1].id;
|
||||
newParent = treeNode.parentId;
|
||||
} else if (moveType == 2) {
|
||||
//选择的是后面
|
||||
//后面
|
||||
newBrother = chilList[index].id;
|
||||
newParent = treeNode.parentId;
|
||||
} else {
|
||||
//选择的是里面
|
||||
//里面 选中的children里面的最后一个
|
||||
newBrother = isEmpty(treeNode.children)
|
||||
? 0
|
||||
: treeNode.children[treeNode.children.length - 1].id;
|
||||
newParent = treeNode.id;
|
||||
}
|
||||
}
|
||||
return {
|
||||
targetId: nativeNode.id,
|
||||
newParent: newParent,
|
||||
newBrother: newBrother,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function treeLink(treeNode: any) {
|
||||
const copyNode = cloneDeep(treeNode)
|
||||
copyNode.children = [];
|
||||
curTreeNode.value = copyNode;
|
||||
// console.log('click', treeNode);
|
||||
// console.log(copyNode.getPath().map((x: any) => x.name));
|
||||
|
||||
curSubjectInfo.value.stage = paramsComp.value?.stage;
|
||||
curSubjectInfo.value.subject = paramsComp.value?.subject;
|
||||
const { stage, subject } = curSubjectInfo.value;
|
||||
emit('on-change', stage, subject, copyNode)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const nodePermissions = computed(() => {
|
||||
if (!curTreeNode.value) return { canCreate: false, canUpdate: false, canMove: false, canDelete: false };
|
||||
const [canCreate, canUpdate, canMove, canDelete] = $utils.unpackPerms(curTreeNode.value.permissions)
|
||||
return {
|
||||
canCreate,
|
||||
canUpdate,
|
||||
canMove,
|
||||
canDelete,
|
||||
}
|
||||
})
|
||||
|
||||
async function onChangeSubject() { }
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.opera-ul {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding-bottom: 10px;
|
||||
|
||||
li {
|
||||
padding: 10px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
li.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#modal-sortTreeNode-id {
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
height: 380px;
|
||||
|
||||
.tree-div {
|
||||
z-index: 1001;
|
||||
border: 1px solid #cacaca;
|
||||
border-radius: 5px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.ztree {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+14
-39
@@ -150,27 +150,10 @@ const childChange = (index: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
/** 根据菜单项映射图标:优先按解析后的 page 判断(平台模块 id 不可依赖),
|
||||
* 兜底保留旧版 modId 对照。图标为本地 bsch-icons(iconfont 掩码类,见 styles/icons.css) */
|
||||
const getMenuIcon = (item?: any): string => {
|
||||
const page: string = item?.page || "";
|
||||
if (page === "/list/new") return "icon-xiugaijl";
|
||||
if (page === "/list") return "icon-fasong";
|
||||
if (page === "/" || page === "/accept") return "icon-sjx";
|
||||
if (page === "/noticelist") return "icon-liebiao";
|
||||
if (page === "/stats") return "icon-tongji";
|
||||
if (page === "/setting") return "icon-shezhi";
|
||||
switch (item?.modId ?? item?.id) {
|
||||
case 1: return "icon-xiugaijl";
|
||||
case 2: return "icon-fasong";
|
||||
case 3: return "icon-sjx";
|
||||
case 4: return "icon-liebiao";
|
||||
case 5: return "icon-wenzhang";
|
||||
case 21: return "icon-shezhi";
|
||||
case 22: return "icon-xiugaijl";
|
||||
default: return "";
|
||||
}
|
||||
};
|
||||
/** 菜单图标:读取用户中心模块管理配置的 YschIcons 图标名(GetUserMod 的 icon 字段,如 list → icon-list)。
|
||||
* 图标掩码类由中央静态通道下发;平台未配置图标时统一兜底 icon-list,保证菜单项恒有图标、整列缩进/行高一致。 */
|
||||
const getMenuIcon = (item?: any): string =>
|
||||
`icon-${String(item?.icon || "").trim() || "list"}`;
|
||||
|
||||
function linkHomePage() {
|
||||
if (baseHost.value) {
|
||||
@@ -187,11 +170,6 @@ function linkHomePage() {
|
||||
<header class="header-container fix">
|
||||
<!-- 云海 logo 徽标(结构与 Notice layout 对齐,皮肤见通道 base.layout.css .logo-badge) -->
|
||||
<span class="logo-badge fl"><img :src="logoUrl" alt="云海" @error="onLogoError" /></span>
|
||||
<div class="fl align-middle icon-box">
|
||||
<div title="首页" class="icon-btn" @click="linkHomePage">
|
||||
<i class="iconfont icon-shouye"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="delimiter fl align-middle"></div>
|
||||
<h4 class="fl align-middle">{{ appName }}</h4>
|
||||
<div class="school-bg"></div>
|
||||
@@ -214,7 +192,7 @@ function linkHomePage() {
|
||||
<li>
|
||||
<a href="javascript:;" @click="childChange(index)">
|
||||
<i :class="item.childState ? 'arrow-down' : 'arrow-up'"></i>
|
||||
<i v-if="getMenuIcon(item)" :class="['iconfont', getMenuIcon(item)]"></i>
|
||||
<i :class="['iconfont', getMenuIcon(item)]"></i>
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</li>
|
||||
@@ -234,7 +212,7 @@ function linkHomePage() {
|
||||
</template>
|
||||
<li v-else :class="item.page == currPath ? 'active' : ''">
|
||||
<a href="javascript:;" @click="linkMenu(item)">
|
||||
<i v-if="getMenuIcon(item)" :class="['iconfont', getMenuIcon(item)]"></i>
|
||||
<i :class="['iconfont', getMenuIcon(item)]"></i>
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</li>
|
||||
@@ -248,7 +226,7 @@ function linkHomePage() {
|
||||
<a href="javascript:;" @click="linkHomePage">
|
||||
<BreadcrumbItem>
|
||||
<span class="crumb-home">
|
||||
<i class="iconfont icon-category"></i>
|
||||
<i class="iconfont icon-shouye"></i>
|
||||
首页
|
||||
</span>
|
||||
</BreadcrumbItem>
|
||||
@@ -288,21 +266,18 @@ function linkHomePage() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 仅覆盖/补充必要的动态样式,其余样式由全局 layout.css 提供 */
|
||||
/* 菜单图标(本地 bsch-icons 掩码类)与文字间距 */
|
||||
/* 仅覆盖/补充必要的动态样式,其余样式由中央静态通道(骨架 + 皮肤)提供 */
|
||||
/* 菜单图标(通道下发的 iconfont 掩码类)与文字间距 */
|
||||
.left-menu a .iconfont {
|
||||
margin-right: 8px;
|
||||
}
|
||||
/* 面包屑「首页」垂直居中:layout.css 遗留的 .path-nav .icon-category{position:relative;top:1px}
|
||||
是给老 iconfont 字体字形做的基线补偿,换成 ysch mask 的 1em 方块后反而把图标顶偏;
|
||||
这里用 inline-flex 真居中并关掉该补偿(不动压缩产物 layout.css,覆盖收在本组件内) */
|
||||
.crumb-home {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
/* 面包屑「首页」与后续级目的文字对齐(2026-10-09 定稿,与 Notice 一致):此前 .crumb-home 用 inline-flex,
|
||||
行内级盒的基线取首项——16px 无字图标(其基线=方块底缘),整盒被顶高约 4px,首页文字比相邻级目高。
|
||||
改回普通行内流:图标以 -0.125em 基线偏移做光学居中,并关掉通道遗留的 .icon-category 基线补偿 */
|
||||
.crumb-home .iconfont {
|
||||
position: static;
|
||||
top: 0;
|
||||
margin-right: 4px;
|
||||
vertical-align: -0.125em;
|
||||
}
|
||||
</style>
|
||||
+1
-4
@@ -13,10 +13,7 @@ import mittBus from "@/utils/mitt";
|
||||
import "view-ui-plus/dist/styles/viewuiplus.css";
|
||||
|
||||
import "@/assets/styles/main.scss";
|
||||
// 本地化的布局/主题样式(原远程 base.layout.beta / _iview_override / default-theme 合并)
|
||||
import "@/assets/styles/layout.css";
|
||||
// 规范 iconfont 图标本地实现(icon-browse / icon-edit / icon-ashbin 等,平台主题不含字体)
|
||||
import "@/assets/styles/icons.css";
|
||||
// 布局/主题/图标由中央静态通道下发(index.html 的 yh-theme-init.js loader + vite /static 代理)
|
||||
//导入自定义指令
|
||||
import directive from "@/utils/directive/index";
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ref, onMounted, onBeforeUnmount, watch, type Ref } from "vue";
|
||||
|
||||
/**
|
||||
* 实测某容器元素的可用高度(px),随尺寸变化实时更新。
|
||||
*
|
||||
* 用途:表格页把 <Table> 放进 flex:1 的 .table-region 里,用本组合式量出该区域
|
||||
* 的真实高度喂给 view-ui 的 `:height`(固定表头需要一个数值高度),从而彻底替代
|
||||
* 旧的 useTableHeight()「视口高 - 一堆写死常量」的算法——加/删工具条、改顶栏高
|
||||
* 都不再需要同步改任何魔法数字。
|
||||
*
|
||||
* @param el 目标容器 ref(模板里 ref 绑定的那个元素)
|
||||
* @returns 响应式高度(px);挂载测量前为 0,调用方可 `:height="h || undefined"` 兜底
|
||||
*/
|
||||
export function useElementHeight(el: Ref<HTMLElement | null>): Ref<number> {
|
||||
const height = ref(0);
|
||||
let ro: ResizeObserver | null = null;
|
||||
|
||||
const measure = () => {
|
||||
if (el.value) height.value = Math.floor(el.value.clientHeight);
|
||||
};
|
||||
|
||||
const observe = () => {
|
||||
ro?.disconnect();
|
||||
if (!el.value || typeof ResizeObserver === "undefined") return;
|
||||
measure();
|
||||
ro = new ResizeObserver(measure);
|
||||
ro.observe(el.value);
|
||||
};
|
||||
|
||||
onMounted(observe);
|
||||
// v-if / v-show 切换导致容器重建时,重新挂观察
|
||||
watch(el, observe);
|
||||
onBeforeUnmount(() => ro?.disconnect());
|
||||
|
||||
return height;
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 表格操作列统一原子(方案:outputs/table-action-buttons/操作列统一方案-20261010.md)
|
||||
*
|
||||
* 三条变化轴各自独立,每加一种形态 = 加 1 个渲染器 + RENDERERS 表加 1 行,不动既有分支:
|
||||
* 轴1 形态 actionIcon / actionText / actionMore —— 只画节点,零策略
|
||||
* 轴2 动作集 normalizeActions —— 纯数据:when 过滤 / disabled 归一 / danger 沉底
|
||||
* 轴3 溢出 splitActions —— 纯函数:直出与「更多」分档,零 DOM 可单测
|
||||
* 组装 actionCell / actionColumn —— 唯一懂 DOM 结构与表格列
|
||||
*
|
||||
* 形态由页面【显式声明】(mode: "icon" | "text",缺省 text),不按动作数自动切换——
|
||||
* 工程间风格区分是既定口径(Notice=图标钮 / UserBase=文字链),各自恒定不漂移。
|
||||
* 「更多▾」只是长尾收纳(动作数 ≥ maxInline+2 时启用),不改变形态、不分档。
|
||||
* 菜单项纯文字不带图标(动作一多找不到贴切图标,文字更准);transfer 到 body,
|
||||
* 避免 fixed/sticky 操作列裁剪与层叠上下文遮盖(原型已实证)。
|
||||
*/
|
||||
import { h, resolveComponent } from "vue";
|
||||
|
||||
export interface ActionItem {
|
||||
/** 稳定标识:溢出分桶按 key 去重,必填 */
|
||||
key: string;
|
||||
label: string;
|
||||
/** 点击回调,normalizeActions 阶段闭包绑定行数据 */
|
||||
on: (row: any) => void;
|
||||
/** iconfont 类名(仅 mode:"icon" 时消费;通道 icons.css 全集见 YschTheme) */
|
||||
icon?: string;
|
||||
/** 危险动作:文字转红(text-error)、恒沉底、「更多」内分隔线隔离 */
|
||||
danger?: boolean;
|
||||
/** 返回 false → 该行不渲染此动作(决定「有没有」) */
|
||||
when?: (row: any) => boolean;
|
||||
/** 直接置灰 */
|
||||
disabled?: boolean;
|
||||
/** 按行判定置灰(决定「能不能点」),disabled 显式值优先 */
|
||||
disabledWhen?: (row: any) => boolean;
|
||||
}
|
||||
|
||||
export interface ActionCellOpts {
|
||||
/** 形态显式声明:icon=图标圆钮 / text=文字链,缺省 text;不按动作数自动切换 */
|
||||
mode?: "icon" | "text";
|
||||
/** 超额动作收进「更多」,默认开(防 5+ 动作页面忘传爆宽) */
|
||||
collapsible?: boolean;
|
||||
/** 直出上限,默认 2;n ≤ maxInline+1 时全直出(3 个动作不进更多) */
|
||||
maxInline?: number;
|
||||
/** 列宽;缺省 icon = n*40+40 / text = 180 */
|
||||
width?: number;
|
||||
align?: "center" | "left" | "right";
|
||||
/** 缺省不固定列(与存量页面一致);需要吸右显式传 "right" */
|
||||
fixed?: "right" | "left" | false;
|
||||
}
|
||||
|
||||
/* ============ 轴 1:渲染器(只画节点,零策略) ============ */
|
||||
|
||||
/** 图标圆钮:Tooltip > div.operating-btn(沿用通道既有 class,主题层零改动) */
|
||||
export function actionIcon(h: any, item: ActionItem) {
|
||||
return h(
|
||||
resolveComponent("Tooltip"),
|
||||
{ content: item.label, placement: "bottom", transfer: true },
|
||||
{
|
||||
default: () => [
|
||||
h("div", {
|
||||
class: `operating-btn iconfont ${item.icon}${item.disabled ? " disabled" : ""}`,
|
||||
onClick: item.disabled ? null : item.on,
|
||||
}),
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 文字链:danger 输出既有 text-error 类对接主题红色 */
|
||||
export function actionText(h: any, item: ActionItem) {
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
class: `operating-link${item.danger ? " text-error" : ""}${item.disabled ? " disabled" : ""}`,
|
||||
onClick: item.disabled ? null : item.on,
|
||||
},
|
||||
item.label,
|
||||
);
|
||||
}
|
||||
|
||||
/** 「更多▾」+ 下拉菜单:iView Dropdown transfer 模式,菜单挂 body */
|
||||
export function actionMore(h: any, items: ActionItem[]) {
|
||||
return h(
|
||||
resolveComponent("Dropdown"),
|
||||
{ class: "op-more", placement: "bottom-end", transfer: true },
|
||||
{
|
||||
default: () => [
|
||||
h("div", { class: "operating-link op-more-label" }, [
|
||||
"更多",
|
||||
h(resolveComponent("Icon"), { type: "ios-arrow-down", class: "op-more-arrow" }),
|
||||
]),
|
||||
],
|
||||
list: () => [
|
||||
h(resolveComponent("DropdownMenu"), {}, {
|
||||
default: () => items.map((it) =>
|
||||
h(
|
||||
resolveComponent("DropdownItem"),
|
||||
{
|
||||
key: it.key,
|
||||
divided: !!(it.danger && items.length > 1),
|
||||
disabled: !!it.disabled,
|
||||
class: it.danger ? "op-item-danger" : "",
|
||||
onClick: () => it.on(),
|
||||
},
|
||||
{ default: () => it.label },
|
||||
),
|
||||
),
|
||||
}),
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/* ============ 轴 2:归一化(纯数据) ============ */
|
||||
export function normalizeActions(items: ActionItem[], row: any): ActionItem[] {
|
||||
return items
|
||||
.filter((it) => (it.when ? it.when(row) : true))
|
||||
.map((it) => ({
|
||||
...it,
|
||||
disabled: it.disabled ?? (it.disabledWhen ? it.disabledWhen(row) : false),
|
||||
on: () => it.on(row),
|
||||
}))
|
||||
.sort((a, b) => Number(!!a.danger) - Number(!!b.danger)); // danger 恒沉底(稳定排序)
|
||||
}
|
||||
|
||||
/* ============ 轴 3:溢出策略(纯函数,可单测、零 DOM) ============ */
|
||||
export function splitActions(items: ActionItem[], maxInline = 2): { inline: ActionItem[]; overflow: ActionItem[] } {
|
||||
if (items.length <= maxInline + 1) return { inline: items, overflow: [] };
|
||||
const inline = items.filter((it) => !it.danger).slice(0, maxInline);
|
||||
const inlineKeys = new Set(inline.map((it) => it.key));
|
||||
return { inline, overflow: items.filter((it) => !inlineKeys.has(it.key)) };
|
||||
}
|
||||
|
||||
/* ============ 组装层 ============ */
|
||||
const RENDERERS: Record<string, (h: any, item: ActionItem) => any> = { icon: actionIcon, text: actionText };
|
||||
|
||||
export function actionCell(h: any, items: ActionItem[], opts: ActionCellOpts = {}) {
|
||||
const mode = opts.mode ?? "text";
|
||||
const { inline, overflow } = (opts.collapsible ?? true)
|
||||
? splitActions(items, opts.maxInline ?? 2)
|
||||
: { inline: items, overflow: [] };
|
||||
|
||||
const render = RENDERERS[mode] || actionText;
|
||||
const children = inline.map((it) => render(h, it));
|
||||
if (overflow.length) children.push(actionMore(h, overflow));
|
||||
|
||||
return h("div", { class: "op-cell" }, children); // flex 横排容器,见 main.scss
|
||||
}
|
||||
|
||||
/** 产出可直接塞进 columns 的操作列对象;列宽按「声明数」估算,when 过滤后只宽不窄 */
|
||||
export function actionColumn(items: ActionItem[], opts: ActionCellOpts = {}) {
|
||||
const mode = opts.mode ?? "text";
|
||||
return {
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: opts.width ?? (mode === "icon" ? items.length * 40 + 40 : 180),
|
||||
align: opts.align ?? "center",
|
||||
...(opts.fixed === undefined ? {} : { fixed: opts.fixed === false ? undefined : opts.fixed }),
|
||||
render: (h: any, p: any) => actionCell(h, normalizeActions(items, p.row), opts),
|
||||
};
|
||||
}
|
||||
@@ -6,33 +6,20 @@
|
||||
* svc-hall-public(BP-R4-3),管理端不该跨应用跳转;管理员查办件要的是"快照 + 填报内容 +
|
||||
* 审批轨迹 + 流程图",一个只读抽屉就够,也不必给管理员看"撤回/重提"这类提交人动作。
|
||||
* 数据面复用用户面同口径接口(/api/apply/{id} 与 /{id}/progress,管理员在守卫白名单内)。
|
||||
* 规范骨架(bsch应用页规范 §1):搜索区 → 表格 + 分页(纯查询页,无操作按钮区)。
|
||||
* 规范骨架(云海子应用前端接入规范 v1.10 §4/§4.4):搜索区 → 表格 + 分页(纯查询页,无工具条)。
|
||||
*/
|
||||
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
import { Message, Tag } from "view-ui-plus";
|
||||
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
|
||||
import { OperatorEnum } from "@/types";
|
||||
import { applyApi, APPLY_STATUS, APPLY_STATUS_COLOR } from "@/api/module/svc";
|
||||
import type { ApplyVo, FlowProgressVo } from "@/types/svc";
|
||||
import { useSite } from "@/utils/site";
|
||||
import { buildFormDataGroups, FormDataGroup } from "@/utils/formDataGroups";
|
||||
import Utils from "@/utils";
|
||||
import { actionColumn } from "@/utils/tableActions";
|
||||
|
||||
const { currentSiteCode, sites, loadSites, setSite } = useSite();
|
||||
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
|
||||
const tableHeight = ref(Utils.useTableHeight({ isSearch: true, isPage: true }));
|
||||
// 首挂载时视口/样式可能尚未定型(body 高偏小甚至 0,且其变化不触发 window resize):
|
||||
// ResizeObserver 观察 body,任何高度来源(样式注入/视口调整)变化即重算,卸载时断开
|
||||
let bodyObserver: ResizeObserver | null = null;
|
||||
const recalcTableHeight = () => {
|
||||
tableHeight.value = Utils.useTableHeight({ isSearch: true, isPage: true });
|
||||
};
|
||||
onMounted(() => {
|
||||
recalcTableHeight();
|
||||
bodyObserver = new ResizeObserver(recalcTableHeight);
|
||||
bodyObserver.observe(document.body);
|
||||
});
|
||||
onUnmounted(() => bodyObserver?.disconnect());
|
||||
const dataParams = ref<{ flowStatus?: number; keyword: string }>({ flowStatus: undefined, keyword: "" });
|
||||
|
||||
const detailVisible = ref(false);
|
||||
@@ -61,9 +48,9 @@ const cols = [
|
||||
{ title: "提交人", key: "submitterName", width: 100 },
|
||||
{ title: "提交时间", key: "submitTime", width: 160 },
|
||||
{ title: "状态", width: 100, align: "center", render: (h: any, p: any) =>
|
||||
h("Tag", { color: APPLY_STATUS_COLOR[p.row.flowStatus] || "default" }, () => APPLY_STATUS[p.row.flowStatus] || p.row.flowStatus) },
|
||||
{ title: "操作", width: 100, align: "center", fixed: "right", render: (h: any, p: any) =>
|
||||
Utils.operatingHandle(h, "详情", "icon-browse", () => openDetail(p.row)) },
|
||||
h(Tag, { color: APPLY_STATUS_COLOR[p.row.flowStatus] || "default" }, () => APPLY_STATUS[p.row.flowStatus] || p.row.flowStatus) },
|
||||
actionColumn([{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => openDetail(r) }],
|
||||
{ mode: "icon", fixed: "right" }),
|
||||
];
|
||||
|
||||
// dataApi 返回 Method(alova usePagination 负责发送与解包),不能调 .send()
|
||||
@@ -134,7 +121,7 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-wrapper yh-fill">
|
||||
<div class="main-container">
|
||||
<!-- 搜索区:站点/状态下拉靠左,关键字即输即搜不设查询按钮 -->
|
||||
<div class="search-wrap fix">
|
||||
@@ -149,7 +136,7 @@ onMounted(async () => {
|
||||
style="width: 200px" @on-search="onSearch" @on-clear="onSearch" />
|
||||
</div>
|
||||
|
||||
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="cols" :tableHeight="tableHeight"
|
||||
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="cols"
|
||||
:autoLoad="false"
|
||||
:formatResponse="(res: any) => ({ total: res.total, list: res.list })" />
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 服务字典管理(modId 24):object/category 双 Tab,引用守护。
|
||||
* 规范骨架(bsch应用页规范 §1):tabs-wrapper + Tabs type=card,Tab 切换只 v-show 内容区,列表状态各自独立。
|
||||
* 规范骨架(云海子应用前端接入规范 v1.10 §4):tabs-wrapper yh-fill → 单卡片 main-container yh-tabs-card →
|
||||
* 线性 Tabs 作卡头 + 每个 pane 一个 tab-body,Tab 切换只 v-show 内容区,列表状态各自独立。
|
||||
*/
|
||||
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
import { Message, Tag } from "view-ui-plus";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import { svcDictApi } from "@/api/module/svc";
|
||||
import type { SvcDictVo } from "@/types/svc";
|
||||
import { useSite } from "@/utils/site";
|
||||
import Utils from "@/utils";
|
||||
import { useElementHeight } from "@/utils/hook/useElementHeight";
|
||||
import { actionColumn } from "@/utils/tableActions";
|
||||
|
||||
const { currentSiteCode, sites, loadSites, setSite } = useSite();
|
||||
const tab = ref<"object" | "category">("object");
|
||||
@@ -18,20 +20,11 @@ const modal = ref(false);
|
||||
const formRef = ref<any>(null);
|
||||
const form = ref<any>({});
|
||||
|
||||
// tabs 页:表格高度在常规骨架上再扣 Tab 条(无分页)
|
||||
const tableHeight = ref(Utils.useTableHeight({ isTabs: true, isOperat: true }));
|
||||
// 首挂载时视口/样式可能尚未定型(body 高偏小甚至 0,且其变化不触发 window resize):
|
||||
// ResizeObserver 观察 body,任何高度来源(样式注入/视口调整)变化即重算,卸载时断开
|
||||
let bodyObserver: ResizeObserver | null = null;
|
||||
const recalcTableHeight = () => {
|
||||
tableHeight.value = Utils.useTableHeight({ isTabs: true, isOperat: true });
|
||||
};
|
||||
onMounted(() => {
|
||||
recalcTableHeight();
|
||||
bodyObserver = new ResizeObserver(recalcTableHeight);
|
||||
bodyObserver.observe(document.body);
|
||||
});
|
||||
onUnmounted(() => bodyObserver?.disconnect());
|
||||
// tab 页无分页:两个 pane 各自实测自身 .table-region(flex:1),不共用高度
|
||||
const objectRegionRef = ref<HTMLElement | null>(null);
|
||||
const objectH = useElementHeight(objectRegionRef);
|
||||
const categoryRegionRef = ref<HTMLElement | null>(null);
|
||||
const categoryH = useElementHeight(categoryRegionRef);
|
||||
|
||||
// 两个 Tab 各自的列表状态(切换只 v-show,互不重置)
|
||||
const objectRows = ref<SvcDictVo[]>([]);
|
||||
@@ -46,11 +39,14 @@ const cols = [
|
||||
{ title: "排序", key: "sortNo", width: 80, align: "center" },
|
||||
{ title: "被引用", key: "referenced", width: 90, align: "center" },
|
||||
{ title: "状态", width: 90, align: "center", render: (h: any, p: any) =>
|
||||
h("Tag", { color: p.row.status === 1 ? "green" : "gray" }, () => (p.row.status === 1 ? "启用" : "停用")) },
|
||||
{ title: "操作", width: 140, align: "center", fixed: "right", render: (h: any, p: any) => [
|
||||
Utils.operatingHandle(h, "编辑", "icon-edit", () => open(p.row)),
|
||||
Utils.operatingHandle(h, "删除", "icon-ashbin", () => remove(p.row)),
|
||||
] },
|
||||
h(Tag, { color: p.row.status === 1 ? "green" : "gray" }, () => (p.row.status === 1 ? "启用" : "停用")) },
|
||||
actionColumn(
|
||||
[
|
||||
{ key: "edit", label: "编辑", icon: "icon-edit", on: (r: any) => open(r) },
|
||||
{ key: "remove", label: "删除", icon: "icon-ashbin", on: (r: any) => remove(r), danger: true },
|
||||
],
|
||||
{ mode: "icon", fixed: "right" },
|
||||
),
|
||||
];
|
||||
|
||||
async function load() {
|
||||
@@ -118,15 +114,16 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tabs-wrapper">
|
||||
<Tabs type="card" :value="tab" @on-click="switchTab">
|
||||
<div class="tabs-wrapper yh-fill">
|
||||
<div class="main-container yh-tabs-card">
|
||||
<Tabs class="tabs-panel" :value="tab" @on-click="switchTab">
|
||||
<TabPane label="服务对象" name="object" />
|
||||
<TabPane label="事项类别" name="category" />
|
||||
</Tabs>
|
||||
|
||||
<div class="main-container" v-show="tab === 'object'">
|
||||
<div class="tab-body" v-show="tab === 'object'">
|
||||
<div class="fix mb-12">
|
||||
<Button class="fl" type="primary" size="large" @click="open()">新建服务对象</Button>
|
||||
<Button class="fl" type="primary" @click="open()">新建服务对象</Button>
|
||||
</div>
|
||||
<div class="search-wrap fix">
|
||||
<Select :model-value="currentSiteCode" class="search-wrap_sel" placeholder="站点"
|
||||
@@ -134,13 +131,15 @@ onMounted(async () => {
|
||||
<Option v-for="s in sites" :key="s.siteCode" :value="s.siteCode">{{ s.name }}({{ s.siteCode }})</Option>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="table-region" ref="objectRegionRef">
|
||||
<Table :columns="cols" :data="objectRows" :loading="objectLoading" class="link-table table-bottom-box"
|
||||
:height="tableHeight" no-data-text="暂无数据" />
|
||||
:height="objectH || undefined" no-data-text="暂无数据" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container" v-show="tab === 'category'">
|
||||
<div class="tab-body" v-show="tab === 'category'">
|
||||
<div class="fix mb-12">
|
||||
<Button class="fl" type="primary" size="large" @click="open()">新建事项类别</Button>
|
||||
<Button class="fl" type="primary" @click="open()">新建事项类别</Button>
|
||||
</div>
|
||||
<div class="search-wrap fix">
|
||||
<Select :model-value="currentSiteCode" class="search-wrap_sel" placeholder="站点"
|
||||
@@ -148,8 +147,11 @@ onMounted(async () => {
|
||||
<Option v-for="s in sites" :key="s.siteCode" :value="s.siteCode">{{ s.name }}({{ s.siteCode }})</Option>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="table-region" ref="categoryRegionRef">
|
||||
<Table :columns="cols" :data="categoryRows" :loading="categoryLoading" class="link-table table-bottom-box"
|
||||
:height="tableHeight" no-data-text="暂无数据" />
|
||||
:height="categoryH || undefined" no-data-text="暂无数据" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal v-model="modal" :title="form.id ? '编辑字典' : '新建字典'" :mask-closable="false">
|
||||
|
||||
@@ -1,36 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 表单管理(modId 25):列表/发布/删除/版本轨迹/绑定明细。编辑走独立全屏设计器(/svc/form/designer)。
|
||||
* 规范骨架(bsch应用页规范 §1):操作按钮区 → 搜索区 → 表格(接口全量返回,无分页,前端关键字过滤)。
|
||||
* 规范骨架(云海子应用前端接入规范 v1.10 §4/§4.2):工具条 → 搜索区 → 表格上方常驻 Alert → 表格(接口全量返回,无分页,前端关键字过滤)。
|
||||
*/
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||
import { computed, nextTick, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Message, Modal } from "view-ui-plus";
|
||||
import { Message, Modal, Tag } from "view-ui-plus";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import { svcFormApi } from "@/api/module/svc";
|
||||
import type { SvcFormItemBrief, SvcFormVo } from "@/types/svc";
|
||||
import { useSite } from "@/utils/site";
|
||||
import Utils from "@/utils";
|
||||
import { useElementHeight } from "@/utils/hook/useElementHeight";
|
||||
import { actionColumn } from "@/utils/tableActions";
|
||||
|
||||
const router = useRouter();
|
||||
const { currentSiteCode, sites, loadSites, setSite } = useSite();
|
||||
const loading = ref(false);
|
||||
const keyword = ref("");
|
||||
const rows = ref<SvcFormVo[]>([]);
|
||||
// 操作按钮区 + 搜索区、无分页
|
||||
const tableHeight = ref(Utils.useTableHeight({ isSearch: true, isOperat: true }));
|
||||
// 首挂载时视口/样式可能尚未定型(body 高偏小甚至 0,且其变化不触发 window resize):
|
||||
// ResizeObserver 观察 body,任何高度来源(样式注入/视口调整)变化即重算,卸载时断开
|
||||
let bodyObserver: ResizeObserver | null = null;
|
||||
const recalcTableHeight = () => {
|
||||
tableHeight.value = Utils.useTableHeight({ isSearch: true, isOperat: true });
|
||||
};
|
||||
onMounted(() => {
|
||||
recalcTableHeight();
|
||||
bodyObserver = new ResizeObserver(recalcTableHeight);
|
||||
bodyObserver.observe(document.body);
|
||||
});
|
||||
onUnmounted(() => bodyObserver?.disconnect());
|
||||
// 操作按钮区 + 搜索区、无分页:表格高度实测 .table-region(flex:1)
|
||||
const formRegionRef = ref<HTMLElement | null>(null);
|
||||
const formH = useElementHeight(formRegionRef);
|
||||
|
||||
const versionsModal = ref(false);
|
||||
const versionRows = ref<SvcFormVo[]>([]);
|
||||
@@ -120,20 +110,23 @@ const cols = [
|
||||
{ title: "名称", key: "name", minWidth: 140 },
|
||||
{ title: "最新版本", key: "version", width: 90, align: "center" },
|
||||
{ title: "状态", width: 90, align: "center", render: (h: any, p: any) =>
|
||||
h("Tag", { color: p.row.status === 1 ? "success" : "default" }, () => statusName(p.row.status)) },
|
||||
h(Tag, { color: p.row.status === 1 ? "success" : "default" }, () => statusName(p.row.status)) },
|
||||
{ title: "被绑定", width: 110, align: "center", render: (h: any, p: any) =>
|
||||
p.row.referenced
|
||||
? h("a", { style: "cursor:pointer", onClick: () => showRefs(p.row) }, `已绑定 ${(p.row.refItems || []).length} 个事项`)
|
||||
: h("span", { style: "color:#999" }, "未绑定") },
|
||||
{ title: "发布时间", key: "publishTime", width: 160 },
|
||||
{ title: "操作", width: 220, align: "center", fixed: "right", render: (h: any, p: any) => {
|
||||
const btns = [Utils.operatingHandle(h, "编辑", "icon-edit", () => editForm(p.row))];
|
||||
if (p.row.status === 0) btns.push(Utils.operatingHandle(h, "发布", "icon-fasong", () => publish(p.row)));
|
||||
if (!p.row.referenced) btns.push(Utils.operatingHandle(h, "去绑定", "icon-peizhi", () => router.push("/svc/item")));
|
||||
btns.push(Utils.operatingHandle(h, "版本", "icon-chakan", () => showVersions(p.row)));
|
||||
btns.push(Utils.operatingHandle(h, "删除", "icon-ashbin", () => remove(p.row)));
|
||||
return btns;
|
||||
} },
|
||||
// §5.2 红线:发布/去绑定原为按行条件 push(忽多忽少)⇒ 改恒在 + disabledWhen 置灰保位
|
||||
actionColumn(
|
||||
[
|
||||
{ key: "edit", label: "编辑", icon: "icon-edit", on: (r: any) => editForm(r) },
|
||||
{ key: "publish", label: "发布", icon: "icon-fasong", on: (r: any) => publish(r), disabledWhen: (r: any) => r.status !== 0 },
|
||||
{ key: "bind", label: "去绑定", icon: "icon-peizhi", on: () => router.push("/svc/item"), disabledWhen: (r: any) => !!r.referenced },
|
||||
{ key: "versions", label: "版本", icon: "icon-chakan", on: (r: any) => showVersions(r) },
|
||||
{ key: "remove", label: "删除", icon: "icon-ashbin", on: (r: any) => remove(r), danger: true },
|
||||
],
|
||||
{ mode: "icon", fixed: "right" },
|
||||
),
|
||||
];
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -143,12 +136,11 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-wrapper yh-fill">
|
||||
<div class="main-container">
|
||||
<!-- 操作按钮区:主操作靠左,说明文字靠右 -->
|
||||
<!-- 工具条(§4.1):只放操作按钮,说明性文案改由表格上方 Alert 承载 -->
|
||||
<div class="fix mb-12">
|
||||
<Button class="fl" type="primary" size="large" @click="createForm">新建表单</Button>
|
||||
<span class="fr bar-tip">拖拽式设计器编辑表单结构,保存为草稿后发布即可在事项发起页使用</span>
|
||||
<Button class="fl" type="primary" @click="createForm">新建表单</Button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区:站点下拉靠左,关键字前端过滤即输即搜 -->
|
||||
@@ -161,8 +153,13 @@ onMounted(async () => {
|
||||
style="width: 200px" />
|
||||
</div>
|
||||
|
||||
<!-- §4.2 页内提示位:表格上方、常驻、show-icon,不加 closable -->
|
||||
<Alert type="info" show-icon>拖拽式设计器编辑表单结构,保存为草稿后发布即可在事项发起页使用</Alert>
|
||||
|
||||
<div class="table-region" ref="formRegionRef">
|
||||
<Table :columns="cols" :data="filteredRows" :loading="loading" class="link-table table-bottom-box"
|
||||
:height="tableHeight" no-data-text="暂无数据" />
|
||||
:height="formH || undefined" no-data-text="暂无数据" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal v-model="refsModal" width="720" :title="`「${refFormName}」被以下事项使用`">
|
||||
@@ -203,10 +200,10 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bar-tip {
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
line-height: 40px;
|
||||
/* 无分页脚:通道只对含 .page-panel 的卡片自动转纵向分栏,本页自声明保证 table-region 撑满 */
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
|
||||
@@ -1,34 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 办事事项列表(modId 23):筛选/发布/下架/删除/同步部门名。
|
||||
* 规范骨架(bsch应用页规范 §1):操作按钮区 → 搜索区 → 表格 + 分页,分页走 TablePageAlovaNew。
|
||||
* 规范骨架(云海子应用前端接入规范 v1.10 §4/§4.4):工具条 → 搜索区 → 表格 + 分页,分页走 TablePageAlovaNew。
|
||||
*/
|
||||
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { Message, Tag } from "view-ui-plus";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
|
||||
import { OperatorEnum } from "@/types";
|
||||
import { svcItemApi, ITEM_STATUS, ITEM_STATUS_COLOR } from "@/api/module/svc";
|
||||
import { useSite } from "@/utils/site";
|
||||
import Utils from "@/utils";
|
||||
import { actionColumn } from "@/utils/tableActions";
|
||||
|
||||
const router = useRouter();
|
||||
const { currentSiteCode, sites, loadSites, setSite } = useSite();
|
||||
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
|
||||
const tableHeight = ref(Utils.useTableHeight({ isSearch: true, isOperat: true, isPage: true }));
|
||||
// 首挂载时视口/样式可能尚未定型(body 高偏小甚至 0,且其变化不触发 window resize):
|
||||
// ResizeObserver 观察 body,任何高度来源(样式注入/视口调整)变化即重算,卸载时断开
|
||||
let bodyObserver: ResizeObserver | null = null;
|
||||
const recalcTableHeight = () => {
|
||||
tableHeight.value = Utils.useTableHeight({ isSearch: true, isOperat: true, isPage: true });
|
||||
};
|
||||
onMounted(() => {
|
||||
recalcTableHeight();
|
||||
bodyObserver = new ResizeObserver(recalcTableHeight);
|
||||
bodyObserver.observe(document.body);
|
||||
});
|
||||
onUnmounted(() => bodyObserver?.disconnect());
|
||||
|
||||
const dataParams = ref<{ status?: number; keyword: string }>({ status: undefined, keyword: "" });
|
||||
|
||||
@@ -43,19 +30,21 @@ const columns = [
|
||||
{ title: "办理方式", width: 90, align: "center", render: (h: any, p: any) => h("span", handleModeName(p.row.handleMode)) },
|
||||
{ title: "在线办理", width: 110, align: "center", render: (h: any, p: any) =>
|
||||
p.row.onlineFlag === 1
|
||||
? h("Tag", { color: "green" }, () => "可在线办理")
|
||||
? h(Tag, { color: "green" }, () => "可在线办理")
|
||||
: h("span", "-") },
|
||||
{ title: "状态", width: 90, align: "center", render: (h: any, p: any) =>
|
||||
h("Tag", { color: ITEM_STATUS_COLOR[p.row.status] || "default" }, () => ITEM_STATUS[p.row.status] || p.row.status) },
|
||||
h(Tag, { color: ITEM_STATUS_COLOR[p.row.status] || "default" }, () => ITEM_STATUS[p.row.status] || p.row.status) },
|
||||
{ title: "浏览量", key: "viewCount", width: 80, align: "center" },
|
||||
{ title: "操作", width: 180, align: "center", fixed: "right", render: (h: any, p: any) => {
|
||||
const btns = [Utils.operatingHandle(h, "编辑", "icon-edit", () => edit(p.row))];
|
||||
btns.push(p.row.status === 1
|
||||
? Utils.operatingHandle(h, "下架", "icon-stop", () => offline(p.row))
|
||||
: Utils.operatingHandle(h, "发布", "icon-fasong", () => publish(p.row)));
|
||||
btns.push(Utils.operatingHandle(h, "删除", "icon-ashbin", () => remove(p.row)));
|
||||
return btns;
|
||||
} },
|
||||
// §5.2 红线:发布/下架按行互斥 ⇒ 两钮恒在、恒一灰一活(disabledWhen 置灰保位,不做条件隐藏)
|
||||
actionColumn(
|
||||
[
|
||||
{ key: "edit", label: "编辑", icon: "icon-edit", on: (r: any) => edit(r) },
|
||||
{ key: "offline", label: "下架", icon: "icon-stop", on: (r: any) => offline(r), disabledWhen: (r: any) => r.status !== 1 },
|
||||
{ key: "publish", label: "发布", icon: "icon-fasong", on: (r: any) => publish(r), disabledWhen: (r: any) => r.status === 1 },
|
||||
{ key: "remove", label: "删除", icon: "icon-ashbin", on: (r: any) => remove(r), danger: true },
|
||||
],
|
||||
{ mode: "icon", fixed: "right" },
|
||||
),
|
||||
];
|
||||
|
||||
// dataApi 返回 Method(alova usePagination 负责发送与解包),不能调 .send()
|
||||
@@ -114,12 +103,12 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-wrapper yh-fill">
|
||||
<div class="main-container">
|
||||
<!-- 操作按钮区:主操作靠左、其余靠右(bsch应用页规范 §1) -->
|
||||
<!-- 工具条(§4.1):操作靠左;本页无导入/导出等数据进出动作 ⇒ 右端留空 -->
|
||||
<div class="fix mb-12">
|
||||
<Button class="fl" type="primary" size="large" @click="edit()">新建事项</Button>
|
||||
<Button class="fr" size="large" @click="syncDept">同步部门名</Button>
|
||||
<Button class="fl" type="primary" @click="edit()">新建事项</Button>
|
||||
<Button class="fl ml-12" @click="syncDept">同步部门名</Button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区:下拉靠左,关键字即输即搜不设查询按钮 -->
|
||||
@@ -135,7 +124,7 @@ onMounted(async () => {
|
||||
style="width: 200px" @on-search="onSearch" @on-clear="onSearch" />
|
||||
</div>
|
||||
|
||||
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" :tableHeight="tableHeight"
|
||||
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns"
|
||||
:autoLoad="false"
|
||||
:formatResponse="(res: any) => ({ total: res.total, list: res.list })" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user