feat(接入规范验收修订轮): 操作列收口text形态+通道撑满高度+主题色批量落地
build / build (push) Successful in 1m58s

- 操作列:group-user/操作日志/开放接口日志/推送日志四页手写 h('a') 迁 actionColumn({mode:"text"}),颜色归通道 var(--brand);推送日志「重试」改 disabledWhen 置灰保位
- group-user 状态列 h('Tag')/h('Badge') 字符串组件名改 import 真组件(Vue3 坑)
- 高度:日志三页根节点改 page-wrapper yh-fill;TablePageAlovaNew 对齐 flow-center 同构版(.table-region + useElementHeight 实测 + 页长记忆);RoleAuthzList 转 tree-wrapper 骨架(外边界/间隙 16)
- 主题色:当前学校/当前组/当前角色、应用管理左类型面板、角色管理卡片操作位硬编码蓝改 var(--brand) 系令牌
- 顶栏房子图标钮删除(§4:首页入口收敛面包屑「首页」)
- 含 2026-10-10 试点未提交件:tableActions.ts 七函数、main.scss .op-cell、AppList/ModuleManage/RoleUserManage 迁移
This commit is contained in:
chenxing
2026-10-10 23:39:32 +08:00
parent 5c7d5d68e4
commit 3b0f29b0d1
17 changed files with 403 additions and 281 deletions
+41
View File
@@ -115,3 +115,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);
}
}
+23 -6
View File
@@ -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,9 +9,10 @@
<slot :name="slotName" v-bind="slotProps" />
</template>
</Table>
</div>
<div class="page-panel">
<div class="fl align-middle">
<div class="fl ml-15 align-middle">
<slot :checkList="selectList" />
</div>
@@ -21,8 +23,8 @@
</template>
<script setup lang="ts">
import { computed, ref, useSlots } from 'vue'
import Utils from '@/utils'
import { computed, ref, useSlots, watch } from 'vue'
import { useElementHeight } from '@/utils/hook/useElementHeight'
import { OperatorEnum } from '@/types'
interface Props {
@@ -44,7 +46,10 @@ const emit = defineEmits<{
}>()
const slots = useSlots()
const tableH = ref(props.tableHeight ?? Utils.useTableHeight())
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[]>([])
@@ -68,18 +73,30 @@ const {
total: res => props.formatResponse(res).total,
data: res => props.formatResponse(res).list,
initialPage: 1,
initialPageSize: 10,
// 页长记忆(bsch 应用页规范 §3:customPageCount localStorage)
initialPageSize: parseInt(localStorage.getItem('customPageCount') || '10') || 10,
immediate: props.autoLoad,
}
)
// 数据重载后清空勾选,避免批量操作用到旧选中行
watch(resp, () => {
if (selectList.value.length) {
selectList.value = []
emit('selection-change', selectList.value)
}
})
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)
const onSizeChange = (s: number) => {
pageSize.value = s
localStorage.setItem('customPageCount', String(s))
}
function refreshData() {
page.value = 1
-13
View File
@@ -191,11 +191,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 home-btn" @click="linkHomePage">
<SvgIcon name="shouye" :size="22" color="#fff" />
</div>
</div>
<div class="delimiter fl align-middle"></div>
<h4 class="fl align-middle">{{ appName }}</h4>
<div class="school-bg"></div>
@@ -295,12 +290,4 @@ function linkHomePage() {
.header-aside .logout svg {
display: block;
}
.home-btn {
display: flex;
align-items: center;
justify-content: center;
}
.home-btn svg {
display: block;
}
</style>
+36
View File
@@ -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;
}
+12 -33
View File
@@ -1,5 +1,5 @@
import { IObj, UseTableSetting } from "@/types/Basics";
import { resolveComponent } from "vue";
import { actionIcon, actionText } from "@/utils/tableActions";
export default class Utils {
/**
* 格式化日期
@@ -90,7 +90,8 @@ export default class Utils {
}
/**
* iview Tooltip封装
* iview Tooltip封装(旧 API,薄封装转调 tableActions.actionIcon,签名不变)
* 2026-10-10 起 D1 修正:禁用态 onClick 置 null(原先禁用仍可点)。
*/
public static operatingHandle(
h: any,
@@ -99,30 +100,10 @@ export default class Utils {
callback: Function,
disabled: boolean = false,
) {
if (disabled) iconClass += " disabled";
return h(
resolveComponent("Tooltip"),
{
content: info,
placement: "bottom",
transfer: true,
},
{
default: () => [
h(
"div",
{
class: `operating-btn iconfont ${iconClass}`,
onClick: callback,
},
null,
),
],
},
);
return actionIcon(h, { key: info, label: info, icon: iconClass, on: () => callback(), disabled });
}
/** 旧 API,薄封装转调 tableActions.actionText(签名不变;第三参仅识别 text-error → danger) */
public static operatingTextHandle(
h: any,
info: string,
@@ -130,15 +111,13 @@ export default class Utils {
callback: Function,
disabled: boolean = false,
) {
if (disabled) className += " disabled";
return h(
"div",
{
class: `operating-link ${className}`,
onClick: disabled ? null : callback,
},
info,
);
return actionText(h, {
key: info,
label: info,
danger: (className || "").includes("text-error"),
on: () => callback(),
disabled,
});
}
/**
+160
View File
@@ -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),
};
}
+2 -2
View File
@@ -372,14 +372,14 @@ onMounted(async () => {
}
.school-name {
color: #2d8cf0;
color: var(--brand);
font-weight: 600;
}
.authz-panel {
position: relative;
flex: 1;
min-height: 320px;
min-height: 0;
padding: 12px 16px;
background: #fff;
border-radius: 4px;
+3 -3
View File
@@ -603,14 +603,14 @@ onMounted(async () => {
}
.group-name {
color: #2d8cf0;
color: var(--brand);
font-weight: 600;
}
.authz-panel {
position: relative;
flex: 1;
min-height: 320px;
min-height: 0;
padding: 12px 16px;
background: #fff;
border-radius: 4px;
@@ -729,7 +729,7 @@ onMounted(async () => {
}
.open-link {
color: #2d8cf0;
color: var(--brand);
&:hover {
text-decoration: underline;
+36 -54
View File
@@ -31,7 +31,7 @@
style="width:220px" @on-search="onSearch" @on-clear="onSearch" />
</div>
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columnsComp" :tableHeight="tableHeight" />
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columnsComp" />
</div>
<!-- 容器组件:新增 / 编辑 -->
@@ -51,7 +51,6 @@
import { ref, computed } from "vue";
defineOptions({ name: "AppList" });
import { useRouter } from "vue-router";
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
import MyConfirm from "@/components/MyConfirm";
import AddAppContainer from "./AddAppContainer.vue";
import EditAppContainer from "./EditAppContainer.vue";
@@ -60,9 +59,9 @@ import TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
import { OperatorEnum } from '@/types';
import AppSort from './AppSort.vue';
import { useAppSchoolScope } from "@/utils/hook/useAppSchoolScope";
import { actionColumn } from "@/utils/tableActions";
const router = useRouter();
const { $utils } = useCurrentInstance();
// 学校管理员上下文:多校显示切换下拉,覆盖仅对所选学校生效(默认并集,写操作须先选定学校)
const { isSchoolMode, multiSchool, schoolList, currentSchoolId, requireSchool } = useAppSchoolScope();
@@ -146,10 +145,6 @@ function onSortSaved() {
tableRef.value?.reloadData(OperatorEnum.sel);
}
const tableHeight = ref(
$utils.useTableHeight({ isOperat: false, isPage: true, isSearch: true })
);
/* ---------------- 表格固定列(不含“应用类型”列) ---------------- */
const columns = [
{ type: "index", title: "序号", width: 70 },
@@ -174,25 +169,41 @@ const columns = [
width: 80,
render: (h: any, { row }: any) => h("span", row.appNature === "BASE" ? "基石" : "业务"),
},
{
title: "操作",
key: "action",
align: 'center',
width: 280,
render: (h: any, { row }: any) => {
actionColumn(
[
// SCHOOL 模式:仅编辑(名称/描述/图标,写学校覆盖表);模块管理/推送设置/删除全部隐藏。
// 基石应用(app_nature=BASE)只做身份注册:模块/推送置灰;删除允许——基石也可下线,
// 但走「输名确认」删除模(危险确认,与 projman 应用删除同款)。
const base = row.appNature === "BASE";
const actions = [$utils.operatingTextHandle(h, '编辑', '', () => handleEdit(row))];
if (!isSchoolMode.value) {
actions.push($utils.operatingTextHandle(h, '模块管理', '', () => goModuleManage(row), base));
actions.push($utils.operatingTextHandle(h, '推送设置', '', () => openPushSetting(row), base));
actions.push($utils.operatingTextHandle(h, '删除', 'text-error', () => handleDelete(row)));
}
return actions;
}
{ key: "edit", label: "编辑", icon: "icon-edit", on: (r: any) => handleEdit(r) },
{
key: "module",
label: "模块管理",
icon: "icon-apps",
on: (r: any) => goModuleManage(r),
when: () => !isSchoolMode.value,
disabledWhen: (r: any) => r.appNature === "BASE",
},
{
key: "push",
label: "推送设置",
icon: "icon-fasong",
on: (r: any) => openPushSetting(r),
when: () => !isSchoolMode.value,
disabledWhen: (r: any) => r.appNature === "BASE",
},
{
key: "del",
label: "删除",
icon: "icon-ashbin",
on: (r: any) => handleDelete(r),
when: () => !isSchoolMode.value,
danger: true,
},
],
// UserBase 恒文字链(工程风格显式声明,不随动作数漂移);
// icon 字段仅留作未来切 mode:"icon" 的备选。4 动作 → 2 直出 + 推送设置/删除进「更多」
{ mode: "text", width: 180 },
),
];
// 条件列:仅在「未筛选应用类型」时插入到“应用名称”之后
@@ -310,7 +321,7 @@ const goModuleManage = (row: any) => {
border-top: 1px solid #eeeeee;
.ivu-btn-text {
color: #577afb;
color: var(--brand);
}
}
@@ -331,37 +342,8 @@ const goModuleManage = (row: any) => {
}
li.active {
color: #577afb;
border-color: #577afb;
color: var(--brand);
border-color: var(--brand);
}
}
.action-cell {
display: flex;
align-items: center;
justify-content: center;
}
.action-link {
cursor: pointer;
color: #577afb;
font-size: 13px;
&:hover {
color: #3d5afe;
}
&.danger {
color: #ed4014;
&:hover {
color: #c51d04;
}
}
}
.action-divider {
margin: 0 8px;
color: #dcdee2;
}
</style>
+11 -17
View File
@@ -54,9 +54,7 @@ import { useRoute, useRouter } from "vue-router";
import { debounce } from "lodash";
import SvgIcon from "@/components/SvgIcon.vue";
import MyConfirm from "@/components/MyConfirm";
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
const { $utils } = useCurrentInstance();
import { actionColumn } from "@/utils/tableActions";
const route = useRoute();
const router = useRouter();
@@ -116,22 +114,18 @@ const columns = [
{ title: "ID", key: "customId", width: 120, align: "center", render: (h: any, { row }: any) => h("span", row.customId || "-") },
{ title: "路径", key: "path", minWidth: 200, render: (h: any, { row }: any) => h("span", row.path || "-") },
{ title: "排序号", key: "sortNo", width: 100, align: "center", render: (h: any, { row }: any) => h("span", row.sortNo ?? "-") },
// UserBase 恒文字链;3 动作 ≤ maxInline+1 → 全直出(与改前形态一致),列宽 220 → 180
actionColumn([
{ key: "edit", label: "编辑", icon: "icon-edit", on: (r: any) => handleEdit(r) },
{
title: "操作",
key: "action",
width: 220,
align: "center",
render: (h: any, { row }: any) => {
const actions: any[] = [
$utils.operatingTextHandle(h, '编辑', '', () => handleEdit(row)),
$utils.operatingTextHandle(h, '删除', 'text-error', () => handleDelete(row)),
];
if (row.level < 5) {
actions.splice(1, 0, $utils.operatingTextHandle(h, '添加子模块', '', () => handleAddChild(row)));
}
return h("div", { style: { display: "flex", justifyContent: "center", gap: "4px" } }, actions);
},
key: "add",
label: "添加子模块",
icon: "icon-tianjia",
on: (r: any) => handleAddChild(r),
when: (r: any) => r.level < 5,
},
{ key: "del", label: "删除", icon: "icon-ashbin", on: (r: any) => handleDelete(r), danger: true },
], { mode: "text", width: 180 }),
];
/* ---------------- 弹窗状态 ---------------- */
+16 -41
View File
@@ -14,7 +14,7 @@
v-model="keyword" @on-search="onSearch" @on-clear="onClear" />
</div>
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" :tableHeight="tableHeight">
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns">
<template #default="{ checkList }">
<Button type="primary" ghost :disabled="!checkList?.length" @click="openAdjust(checkList)">调整组</Button>
<Button type="warning" ghost class="ml-15" :disabled="!checkList?.length" @click="openBatchStatus(checkList)">设置状态</Button>
@@ -59,7 +59,7 @@
<script lang="ts" setup>
import { ref, computed } from "vue";
import { Message } from "view-ui-plus";
import { Message, Tag, Badge } from "view-ui-plus";
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
import MyConfirm from "@/components/MyConfirm";
import TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
@@ -70,10 +70,11 @@ import EditUserContainer from './user/EditUserContainer.vue';
import AdjustGroupContainer from './AdjustGroupContainer.vue';
import BatchImportUser from './user/BatchImportUser.vue';
import UserSort from './user/UserSort.vue';
import { actionColumn } from "@/utils/tableActions";
defineOptions({ name: "GroupUser" });
const { $utils, $conf } = useCurrentInstance();
const { $conf } = useCurrentInstance();
const treeApi = () => Apis.SysGroup.getTree();
@@ -146,8 +147,6 @@ const tableApi = (params: { pageIndex: number; pageSize: number }) => {
});
};
const tableHeight = ref($utils.useTableHeight({ isOperat: false, isPage: true, isSearch: true }))
/* ================= 表格列 ================= */
const columns = [
{ type: 'selection', width: 55, align: 'center' },
@@ -164,49 +163,29 @@ const columns = [
{
title: '状态', key: 'disabled', width: 130, align: 'center',
render: (h: any, { row }: any) => {
if (row.disabled) return h('Tag', { color: 'default' }, '禁用');
// Vue3 渲染函数须 import 真组件:h('Tag') 会建未知原生元素(无样式不渲染)
if (row.disabled) return h(Tag, { color: 'default' }, () => '禁用');
if (row.groupStatus === 1) {
// 正常 Tag + 转出徽标(view-ui-plus Badge status 模式:圆点 + 文本)
return h('div', { style: 'display:inline-flex;align-items:center;gap:6px' }, [
h('Tag', { color: 'success' }, '正常'),
h('Badge', { props: { status: 'warning', text: '转出' } }),
h(Tag, { color: 'success' }, () => '正常'),
h(Badge, { status: 'warning', text: '转出' }),
]);
}
return h('Tag', { color: 'success' }, '正常');
return h(Tag, { color: 'success' }, () => '正常');
},
},
{
title: '排序号', key: 'sortNo', width: 90, align: 'center',
render: (h: any, { row }: any) => h('span', row.sortNo == null ? '-' : row.sortNo),
},
{
title: '操作', key: 'action', width: 220, align: 'center', fixed: 'right' as const,
render: (h: any, { row }: any) => {
const opList: any[] = [
h('a', {
style: { marginRight: '8px', color: '#577afb' },
onClick: () => handleEdit(row),
}, '编辑'),
];
// 仅「单组浏览」(withSub=0)时开放排序:此时每个用户都直接属于当前组,排序号语义明确;
// 含子组搜索(withSub=1)下用户可能属于子组,排序号归属不清,故隐藏。
if (dataParams.value.withSub === 0) {
opList.push(
h('a', {
style: { marginRight: '8px', color: '#2d8cf0' },
onClick: () => openSort(row),
}, '排序'),
);
}
opList.push(
h('a', {
style: { color: '#ed4014' },
onClick: () => handleDelete(row),
}, '删除'),
);
return h('div', opList);
},
},
// 文字链形态:颜色走通道 .operating-link = var(--brand),随主题档位变化;
// 排序仅「单组浏览」开放——withSub 是与行数据无关的全局模式开关,用 when 整组隐藏合规
actionColumn([
{ key: 'edit', label: '编辑', on: (r: any) => handleEdit(r) },
{ key: 'sort', label: '排序', on: (r: any) => openSort(r), when: () => dataParams.value.withSub === 0 },
{ key: 'del', label: '删除', on: (r: any) => handleDelete(r), danger: true },
], { mode: 'text', width: 180, fixed: 'right' }),
];
/* ================= 导出组用户 ================= */
@@ -368,10 +347,6 @@ function onSortSaved() {
</script>
<style lang="scss">
.group-user-page .page-panel {
left: 348px !important;
}
/* 左侧树容器改为 flex 纵向布局并收起自身滚动,把滚动条收敛到树节点区域
(滚动体是 MyTree 内部的 .ztree,见 components/MyTree.vue),
避免树过长时把顶部 opera-ul 操作按钮条滚出屏幕;仅作用于本页,不影响全局 .tree-container */
+7 -19
View File
@@ -1,5 +1,5 @@
<template>
<div class="main-wrapper">
<div class="page-wrapper yh-fill">
<div class="main-container">
<div class="search-wrap fix">
<Input v-model="dataParams.keyword" placeholder="输入接口名/应用名/appKey搜索" clearable suffix="ios-search"
@@ -26,7 +26,7 @@
<Button type="primary" class="ml-12" @click="handleSearch">查询</Button>
</div>
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" :tableHeight="tableHeight" />
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" />
</div>
<OapiLogDetailModal v-model="detailVisible" :row="currentRow" />
@@ -35,15 +35,13 @@
<script setup lang="ts">
import { ref } from 'vue';
import useCurrentInstance from '@/utils/hook/useCurrentInstance';
import TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
import OapiLogDetailModal from './OapiLogDetailModal.vue';
import Utils from '@/utils';
import { actionColumn } from '@/utils/tableActions';
defineOptions({ name: 'OapiLogList' });
const { $utils } = useCurrentInstance();
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
interface DataParams {
@@ -98,8 +96,6 @@ const tableApi = (params: { pageIndex: number; pageSize: number }) => {
});
};
const tableHeight = ref($utils.useTableHeight({ isOperat: false, isPage: true, isSearch: true }));
const logLevelMap: Record<number, { text: string; className: string }> = {
1: { text: '重要', className: 'text-error' },
2: { text: '次要', className: 'text-warning' },
@@ -164,18 +160,10 @@ const columns = [
width: 180,
render: (h: any, { row }: any) => h('span', row.createTime ? Utils.dateFormat(row.createTime, 'yyyy-MM-dd HH:mm:ss') : '-'),
},
{
title: '操作',
key: 'action',
width: 100,
align: 'center',
fixed: 'right' as const,
render: (h: any, { row }: any) =>
h('a', {
style: { color: '#577afb' },
onClick: () => openDetail(row),
}, '查看详情'),
},
// 文字链形态:颜色走通道 .operating-link = var(--brand),随主题档位变化
actionColumn([
{ key: 'detail', label: '查看详情', on: (r: any) => openDetail(r) },
], { mode: 'text', width: 100, fixed: 'right' }),
];
const detailVisible = ref(false);
+8 -20
View File
@@ -1,5 +1,5 @@
<template>
<div class="main-wrapper">
<div class="page-wrapper yh-fill">
<div class="main-container">
<div class="search-wrap fix">
<Input v-model="dataParams.keyword" placeholder="接口名称 / 用户名" clearable suffix="ios-search"
@@ -27,7 +27,7 @@
<Button type="primary" class="ml-12" @click="handleSearch">查询</Button>
</div>
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" :tableHeight="tableHeight" />
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" />
</div>
<OperationLogDetailModal v-model="detailVisible" :row="currentRow" />
@@ -36,15 +36,13 @@
<script setup lang="ts">
import { ref } from 'vue';
import useCurrentInstance from '@/utils/hook/useCurrentInstance';
import TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
import OperationLogDetailModal from './OperationLogDetailModal.vue';
import Utils from '@/utils';
import { actionColumn } from '@/utils/tableActions';
defineOptions({ name: 'OperationLogList' });
const { $utils } = useCurrentInstance();
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
interface DataParams {
@@ -99,8 +97,6 @@ const tableApi = (params: { pageIndex: number; pageSize: number }) => {
});
};
const tableHeight = ref($utils.useTableHeight({ isOperat: false, isPage: true, isSearch: true }));
const logLevelMap: Record<number, { text: string; className: string }> = {
1: { text: '重要', className: 'text-error' },
2: { text: '次要', className: 'text-warning' },
@@ -160,23 +156,15 @@ const columns = [
render: (h: any, { row }: any) => h('span', row.responseSize != null && row.responseSize > 0 ? Utils.getFileSize(row.responseSize) : '-'),
},
{
title: '操作时间ed',
title: '操作时间',
key: 'createTime',
width: 180,
render: (h: any, { row }: any) => h('span', row.createTime ? Utils.dateFormat(row.createTime, 'yyyy-MM-dd HH:mm:ss') : '-'),
},
{
title: '操作',
key: 'action',
width: 100,
align: 'center',
fixed: 'right' as const,
render: (h: any, { row }: any) =>
h('a', {
style: { color: '#577afb' },
onClick: () => openDetail(row),
}, '查看详情'),
},
// 文字链形态:颜色走通道 .operating-link = var(--brand),随主题档位变化
actionColumn([
{ key: 'detail', label: '查看详情', on: (r: any) => openDetail(r) },
], { mode: 'text', width: 100, fixed: 'right' }),
];
const detailVisible = ref(false);
+8 -32
View File
@@ -1,5 +1,5 @@
<template>
<div class="main-wrapper">
<div class="page-wrapper yh-fill">
<div class="main-container">
<div class="search-wrap fix">
<Select v-model="dataParams.appKey" class="ml-12" clearable filterable placeholder="应用"
@@ -27,7 +27,7 @@
<Button type="primary" class="ml-12" @click="handleSearch">查询</Button>
</div>
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" :tableHeight="tableHeight" />
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" />
</div>
<PushLogDetailModal v-model="detailVisible" :row="currentRow" />
@@ -37,15 +37,13 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Message } from 'view-ui-plus';
import useCurrentInstance from '@/utils/hook/useCurrentInstance';
import TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
import Utils from '@/utils';
import PushLogDetailModal from './PushLogDetailModal.vue';
import { actionColumn } from '@/utils/tableActions';
defineOptions({ name: 'PushLog' });
const { $utils } = useCurrentInstance();
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
const interfaceOptions = [
@@ -113,8 +111,6 @@ const tableApi = (params: { pageIndex: number; pageSize: number }) => {
});
};
const tableHeight = ref($utils.useTableHeight({ isOperat: false, isPage: true, isSearch: true }));
const columns = [
{ type: 'index', title: '序号', width: 70, align: 'center' },
{ title: '应用', key: 'appName', ellipsis: true, tooltip: true },
@@ -163,23 +159,11 @@ const columns = [
align: 'right',
render: (h: any, { row }: any) => h('span', row.payloadSize != null && row.payloadSize > 0 ? Utils.getFileSize(row.payloadSize) : '-'),
},
{
title: '操作',
key: 'action',
width: 150,
align: 'left',
render: (h: any, { row }: any) => {
const actions = [
h('a', { style: { color: '#577afb', marginRight: '12px' }, onClick: () => openDetail(row) }, '查看详情'),
];
if (row.success !== 1) {
actions.push(
h('a', { style: { color: '#ed4014' }, onClick: () => handleRetry(row) }, '重试')
);
}
return h('span', actions);
},
},
// 重试按行数据(成功与否)变化 → disabledWhen 置灰保位(§5.2 按钮位置稳定性红线),禁止 when 隐藏
actionColumn([
{ key: 'detail', label: '查看详情', on: (r: any) => openDetail(r) },
{ key: 'retry', label: '重试', on: (r: any) => handleRetry(r), danger: true, disabledWhen: (r: any) => r.success === 1 },
], { mode: 'text', width: 150 }),
];
const detailVisible = ref(false);
@@ -225,12 +209,4 @@ loadApps();
vertical-align: middle;
}
}
.text-success {
color: #19be6b;
}
.text-error {
color: #ed4014;
}
</style>
+22 -21
View File
@@ -1,7 +1,7 @@
<template>
<div class="page-wrapper role-authz-wrapper">
<div class="page-wrapper tree-wrapper role-authz-wrapper">
<!-- 左:角色列表(可滚动,应对大量角色;顶部 2 个筛选) -->
<div class="role-list-panel">
<div class="tree-container role-list-panel">
<div class="role-filter">
<Select v-model="filterAppId" placeholder="按绑定应用筛选" filterable clearable
@on-change="onFilterChange">
@@ -31,7 +31,7 @@
</div>
<!-- 右:选中角色的授权面板(应用授权 / 用户管理) -->
<div class="authz-panel">
<div class="main-container authz-panel">
<div v-if="selectedRole" class="authz-inner">
<div class="authz-bar">
<span class="authz-title">当前角色:<b>{{ selectedRole.name }}</b></span>
@@ -152,19 +152,12 @@ function goManage() {
</script>
<style lang="scss" scoped>
.role-authz-wrapper {
display: flex;
height: 100%;
background: #f5f7fa;
}
/* 通道 tree-wrapper 骨架:外边界 16 / 两卡间隙 16 / 等高 stretch,高度对齐列表页 */
.role-list-panel {
width: 300px;
flex-shrink: 0;
padding: 0;
display: flex;
flex-direction: column;
background: #ffffff;
border-right: 1px solid #ebedf0;
overflow: hidden;
}
@@ -196,12 +189,12 @@ function goManage() {
transition: all 0.15s;
&:hover {
border-color: #b8c6ff;
border-color: var(--brand-2);
}
&.active {
border-color: #577afb;
background: #f3f6ff;
border-color: var(--brand);
background: var(--brand-soft);
}
}
@@ -235,18 +228,21 @@ function goManage() {
border-top: 1px solid #eeeeee;
.ivu-btn-text {
color: #577afb;
color: var(--brand);
}
}
.authz-panel {
flex: 1;
min-width: 0;
overflow: auto;
padding: 0 16px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.authz-inner {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
padding-top: 12px;
}
@@ -255,10 +251,11 @@ function goManage() {
align-items: center;
gap: 10px;
padding: 8px 0 4px;
flex: none;
}
.authz-title b {
color: #577afb;
color: var(--brand);
}
.authz-app {
@@ -268,9 +265,13 @@ function goManage() {
.authz-tabs {
margin-top: 6px;
flex: none;
}
.authz-tab-content {
flex: 1;
min-height: 0;
overflow: auto;
padding-top: 8px;
}
</style>
+2 -2
View File
@@ -158,10 +158,10 @@ function goAuthz() {
text-align: center;
font-size: 12px;
cursor: pointer;
color: #577afb;
color: var(--brand);
&:hover {
color: #6c91f0;
color: var(--brand-2);
}
}
+5 -7
View File
@@ -16,11 +16,9 @@
import { ref, computed, watch } from "vue";
import { Message } from "view-ui-plus";
import Apis from "@/api";
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
import MyConfirm from "@/components/MyConfirm";
import RoleUserBindModal from "./RoleUserBindModal.vue";
const { $utils } = useCurrentInstance();
import { actionColumn } from "@/utils/tableActions";
const props = defineProps<{
roleId: number;
@@ -44,10 +42,10 @@ const columns = [
{ title: "账号", key: "account", width: 110 },
{ title: "姓名", key: "realName", width: 90 },
{ title: "部门", key: "groupPath", minWidth: 200 },
{
title: "操作", key: "action", width: 80, align: "center",
render: (h: any, { row }: any) => $utils.operatingTextHandle(h, "删除", "text-error", () => removeUser(row)),
},
// UserBase 恒文字链(工程风格显式声明):单删除动作保持红色文字链,列宽 80 不变
actionColumn([
{ key: "del", label: "删除", icon: "icon-ashbin", on: (r: any) => removeUser(r), danger: true },
], { mode: "text", width: 80 }),
];
async function loadUsers() {