refactor(theme): A 类主题通道接入+本地样式回收、操作列统一 actionColumn、顶栏三态头像
build / build (push) Successful in 2m9s

- P1 通道:view-ui-plus 锁 1.3.24(A 类 loader + vite /static dev 代理核验,五档/断源降级走查)
- P2 回收:删本地 layout.css/icons.css(+bak) 并清 main.ts import;骨架六类对齐 + 品牌色 var(--brand) 化;index CSS 779→393kB
- P3 操作列:新增 utils/tableActions.ts(同构 UserBase,7 函数+2 表,无 AUTO_MODE);7 页迁 actionColumn text 形态;旧 operatingHandle/operatingTextHandle 薄封装保留、签名不变
- P4 外观:接入三态头像 avatar.ts+UserAvatar.vue(同步自 YschTheme),顶栏裸 img 替换;外观三键红线零写
- 失效注释订正(layout.vue/def-editor)
This commit is contained in:
chenxing
2026-10-10 21:24:39 +08:00
parent ed5f818c1a
commit 43753f4f59
27 changed files with 508 additions and 1165 deletions
+1 -1
View File
@@ -21,7 +21,7 @@
"pdfjs-dist": "^4.10.38", "pdfjs-dist": "^4.10.38",
"pinia": "^2.1.4", "pinia": "^2.1.4",
"sass": "^1.64.1", "sass": "^1.64.1",
"view-ui-plus": "^1.3.16", "view-ui-plus": "1.3.24",
"vite-plugin-mock": "2.9.6", "vite-plugin-mock": "2.9.6",
"vue": "^3.4.19", "vue": "^3.4.19",
"vue-router": "4", "vue-router": "4",
+1 -1
View File
@@ -28,7 +28,7 @@
"pdfjs-dist": "^4.10.38", "pdfjs-dist": "^4.10.38",
"pinia": "^2.1.4", "pinia": "^2.1.4",
"sass": "^1.64.1", "sass": "^1.64.1",
"view-ui-plus": "^1.3.16", "view-ui-plus": "1.3.24",
"vite-plugin-mock": "2.9.6", "vite-plugin-mock": "2.9.6",
"vue": "^3.4.19", "vue": "^3.4.19",
"vue-router": "4", "vue-router": "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
+42 -1
View File
@@ -107,7 +107,7 @@ h4.subtitle {
background-position: top center; background-position: top center;
background-repeat: no-repeat; background-repeat: no-repeat;
border: none; border: none;
color: #577afb; color: var(--brand);
margin: 0 auto; margin: 0 auto;
} }
@@ -115,3 +115,44 @@ h4.subtitle {
margin-right: 4px; margin-right: 4px;
margin-bottom: 2px; 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);
}
}
+102
View File
@@ -0,0 +1,102 @@
<script lang="ts" setup>
/* 默认头像三态组件(YschTheme 公共源,scripts/sync-apps.mjs 下发三端 src/components/UserAvatar.vue):
头像 URL 非空 → <img>;否则 style==="text" 且能取到字 → 首字;否则 → 剪影(纯 CSS/SVG,零资源文件)。
取值链/取字契约见《设置页修复与默认头像落地方案》§10.1/§10.2;样式全在本文件 <style scoped>(唯一 CSS 源,
不进通道 icons.src.css——通道 CSS 对 UserHome 不可达,内联分发才能消双份漂移,见同方案 §10.4)。
根元素即 .yh-avatar 圆:尺寸内联必须挂圆本身(挂外层 span 会被 flex 拉成椭圆,§12 踩坑)。 */
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { pickAvatarText, resolveAvatarStyle } from "@/utils/avatar";
const props = withDefaults(
defineProps<{
/** 头像 URL:非空直接 <img>(三态第一优先) */
src?: string | null;
/** 取字来源(首字态用):经 pickAvatarText 判定,取不到字回落剪影 */
name?: string | null;
/** 直径 px;font-size = round(size * 0.44) */
size?: number;
/** 配色场景:light=页面浅底(品牌实底+白内容);brand=header 品牌色底(剪影 S1 玻璃底 / 首字 S2 白底反色,§10.3 分档) */
placement?: "light" | "brand";
}>(),
{ src: null, name: null, size: 28, placement: "light" },
);
const text = computed(() => (props.src ? "" : pickAvatarText(props.name)));
/* 外观偏好异步补齐(yh-theme-init 在挂载后拉服务端真源收敛 localStorage 键)会在
document 上派发 yh-appearance-updated;tick 自增让 style 依赖失效重读键,三态即时切换(无需刷新)。 */
const appearanceTick = ref(0);
const onAppearanceUpdated = () => { appearanceTick.value += 1; };
onMounted(() => document.addEventListener("yh-appearance-updated", onAppearanceUpdated));
onBeforeUnmount(() => document.removeEventListener("yh-appearance-updated", onAppearanceUpdated));
const style = computed(() => {
appearanceTick.value;
return resolveAvatarStyle();
});
/** 首字态 = style==="text" 且能取到字(契约 §10.2:二者缺一即剪影,取到字但档位是剪影时绝不能出字) */
const showText = computed(() => style.value === "text" && !!text.value);
const rootClass = computed(() => [
"yh-avatar",
props.placement === "brand" ? "yh-avatar--brand" : "yh-avatar--light",
{ "is-text": showText.value },
]);
const rootStyle = computed(() => ({
width: `${props.size}px`,
height: `${props.size}px`,
fontSize: `${Math.round(props.size * 0.44)}px`,
}));
</script>
<template>
<span :class="rootClass" :style="rootStyle">
<img v-if="src" class="yh-avatar__img" :src="src" alt="" />
<template v-else-if="showText">{{ text }}</template>
<svg v-else class="yh-avatar__sil" viewBox="0 0 1024 1024" aria-hidden="true">
<circle cx="512" cy="399" r="174" />
<ellipse cx="512" cy="946" rx="420" ry="240" />
</svg>
</span>
</template>
<style scoped>
.yh-avatar {
position: relative;
border-radius: 50%;
overflow: hidden;
display: inline-grid;
place-items: center;
flex: 0 0 auto;
font-weight: 600;
line-height: 1;
user-select: none;
}
.yh-avatar__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* 剪影 SVG:容器圆 overflow:hidden 裁切肩部溢出(禁用 clipPath);overflow:visible 放行 viewBox 外形状 */
.yh-avatar__sil {
display: block;
width: 100%;
height: 100%;
overflow: visible;
fill: currentColor;
}
/* 页面内(浅底)——品牌实底 + 白内容 */
.yh-avatar--light {
background: var(--brand, #577afb);
color: #fff;
}
/* header(品牌色底)——与页面内反向:剪影档 = S1 半透明白,首字档 = S2 纯白底 + 品牌色字 */
.yh-avatar--brand {
background: rgba(255, 255, 255, 0.22);
color: #fff;
}
.yh-avatar--brand.is-text {
background: #fff;
color: var(--brand, #577afb);
}
</style>
+2 -1
View File
@@ -46,7 +46,7 @@
> >
<path <path
d="M138.935 256.078v511.844h63.42V256.078h-63.42z m126.842 0v511.844h127.215V256.078H265.777zM885.064 512L583.254 64.323v191.755H456.414v511.844h126.842v191.755L885.064 512z" d="M138.935 256.078v511.844h63.42V256.078h-63.42z m126.842 0v511.844h127.215V256.078H265.777zM885.064 512L583.254 64.323v191.755H456.414v511.844h126.842v191.755L885.064 512z"
fill="#2d8cf0" fill="currentColor"
></path> ></path>
</svg> </svg>
</div> </div>
@@ -488,6 +488,7 @@ export default {
position: relative; position: relative;
top: 50%; top: 50%;
margin-top: -17px; margin-top: -17px;
color: var(--brand);
} }
.auto-box { .auto-box {
width: 200px; width: 200px;
+6 -5
View File
@@ -4,6 +4,7 @@ import { useRoute, useRouter } from "vue-router";
import { Message } from "view-ui-plus"; import { Message } from "view-ui-plus";
import { resolveMenuPage } from "@/utils/menuRoute"; import { resolveMenuPage } from "@/utils/menuRoute";
import defaultLogo from "@/assets/image/yunhai-logo.svg"; import defaultLogo from "@/assets/image/yunhai-logo.svg";
import UserAvatar from "@/components/UserAvatar.vue";
const props = defineProps<{ layoutData: Record<string, any> }>(); const props = defineProps<{ layoutData: Record<string, any> }>();
const route = useRoute(); const route = useRoute();
@@ -154,7 +155,7 @@ const childChange = (index: number) => {
}; };
/** 根据菜单项映射图标:优先按解析后的 page 判断(平台模块 id 不可依赖), /** 根据菜单项映射图标:优先按解析后的 page 判断(平台模块 id 不可依赖),
* 兜底保留旧版 modId 对照。图标为本地 bsch-icons(iconfont 掩码类,见 styles/icons.css) */ * 兜底保留旧版 modId 对照。图标为通道 iconfont 掩码类(A 类接入后由 YschTheme 发版 icons.css 提供,本地副本已回收) */
const getMenuIcon = (item?: any): string => { const getMenuIcon = (item?: any): string => {
const page: string = item?.page || ""; const page: string = item?.page || "";
if (page === "/list/new") return "icon-xiugaijl"; if (page === "/list/new") return "icon-xiugaijl";
@@ -199,7 +200,7 @@ function linkHomePage() {
<h4 class="fl align-middle">{{ appName }}</h4> <h4 class="fl align-middle">{{ appName }}</h4>
<div class="school-bg"></div> <div class="school-bg"></div>
<div class="header-aside fr"> <div class="header-aside fr">
<img :src="userInfo.avatar" class="fl align-middle" @click="linkUserInfo" /> <UserAvatar class="fl align-middle" :src="userInfo.avatar" :name="userInfo.name" :size="28" placement="brand" @click="linkUserInfo" />
<a class="name fl align-middle text-ellipsis" href="javascript:void(0);" @click="linkUserInfo"> <a class="name fl align-middle text-ellipsis" href="javascript:void(0);" @click="linkUserInfo">
{{ userInfo.name }} {{ userInfo.name }}
</a> </a>
@@ -291,14 +292,14 @@ function linkHomePage() {
</template> </template>
<style scoped> <style scoped>
/* 仅覆盖/补充必要的动态样式,其余样式由全局 layout.css 提供 */ /* 仅覆盖/补充必要的动态样式,其余样式由主题通道 base.layout.css 提供 */
/* 菜单图标(本地 bsch-icons 掩码类)与文字间距 */ /* 菜单图标(本地 bsch-icons 掩码类)与文字间距 */
.left-menu a .iconfont { .left-menu a .iconfont {
margin-right: 8px; margin-right: 8px;
} }
/* 面包屑「首页」垂直居中:layout.css 遗留的 .path-nav .icon-category{position:relative;top:1px} /* 面包屑「首页」垂直居中:通道 base.layout.css 的 .path-nav .icon-category{position:relative;top:1px}
是给老 iconfont 字体字形做的基线补偿,换成 ysch mask 的 1em 方块后反而把图标顶偏; 是给老 iconfont 字体字形做的基线补偿,换成 ysch mask 的 1em 方块后反而把图标顶偏;
这里用 inline-flex 真居中并关掉该补偿(不动压缩产物 layout.css,覆盖收在本组件内) */ 这里用 inline-flex 真居中并关掉该补偿(不改通道产物,覆盖收在本组件内) */
.crumb-home { .crumb-home {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
-4
View File
@@ -13,10 +13,6 @@ import mittBus from "@/utils/mitt";
import "view-ui-plus/dist/styles/viewuiplus.css"; import "view-ui-plus/dist/styles/viewuiplus.css";
import "@/assets/styles/main.scss"; 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";
//导入自定义指令 //导入自定义指令
import directive from "@/utils/directive/index"; import directive from "@/utils/directive/index";
+94
View File
@@ -0,0 +1,94 @@
/**
* 默认头像样式(首字 / 剪影)公共源(YschTheme,scripts/sync-apps.mjs 下发三端 src/utils/avatar.ts)。
* 取值链与主题同构(theme-core.js resolveEffectiveThemeState 的合成链写法):
* URL ?yh-avatar=text|silhouette(仅预览/排障覆盖:只影响读、绝不写回)
* > localStorage["yh-avatar"](用户自选,纯字符串)
* > localStorage["yh-avatar-sch-default"](学校默认,UserHome 登录后写入)
* > "text"(系统默认)
* 值域恒为 "text" | "silhouette",键缺失/值非法均静默回落下一级,不报错不阻塞渲染。
* 渲染三态与取字规则的唯一契约见 UserHome《设置页修复与默认头像落地方案》§10.1/§10.2。
*/
export type AvatarStyle = "text" | "silhouette";
interface StorageLike {
getItem(k: string): string | null;
setItem(k: string, v: string): void;
removeItem(k: string): void;
}
export const AVATAR_STORAGE_KEY = "yh-avatar";
/** 学校默认头像存储键(UserHome 登录时写入;值格式与用户键相同,仅无"用户自选"语义) */
export const SCHOOL_DEFAULT_AVATAR_STORAGE_KEY = "yh-avatar-sch-default";
export const AVATAR_STYLES: readonly AvatarStyle[] = ["text", "silhouette"];
export const DEFAULT_AVATAR_STYLE: AvatarStyle = "text";
/** 值合法才采用(用户键/学校键同一判据;脏数据不生效) */
function readAvatarStyleOrNull(raw: string | null): AvatarStyle | null {
return raw === "text" || raw === "silhouette" ? raw : null;
}
/** URL 覆盖参数(?yh-avatar=text|silhouette):非法值忽略,且只影响"读哪一档"、绝不写回 localStorage */
function readAvatarOverride(): AvatarStyle | null {
try {
const search = (globalThis.location && globalThis.location.search) || "";
const m = new RegExp("[?&]yh-avatar=([^&]*)").exec(search);
return m ? readAvatarStyleOrNull(decodeURIComponent(m[1])) : null;
} catch {
/* 无 location 环境(测试/SSR):无覆盖 */
return null;
}
}
/**
* 生效样式合成(与 resolveEffectiveThemeState 同构):
* URL 覆盖 > localStorage["yh-avatar"](用户自选)> localStorage["yh-avatar-sch-default"](学校默认)
* > DEFAULT_AVATAR_STYLE。
*/
export function resolveAvatarStyle(storage: StorageLike | undefined = globalThis.localStorage): AvatarStyle {
let style: AvatarStyle | null = null;
if (storage) {
style = readAvatarStyleOrNull(storage.getItem(AVATAR_STORAGE_KEY));
if (!style) style = readAvatarStyleOrNull(storage.getItem(SCHOOL_DEFAULT_AVATAR_STORAGE_KEY));
}
if (!style) style = DEFAULT_AVATAR_STYLE;
return readAvatarOverride() || style;
}
/** 保存用户自选(直写 localStorage,无响应式;调用方操作后自行刷新 UI) */
export function saveAvatarStyle(style: AvatarStyle, storage: StorageLike | undefined = globalThis.localStorage): void {
try {
storage?.setItem(AVATAR_STORAGE_KEY, style);
} catch {
/* 隐私模式/配额失败静默:仅丢持久化,不丢本次会话生效 */
}
}
/** 跟随学校默认 = 删用户自选键(之后回落学校默认/系统默认) */
export function clearAvatarStyle(storage: StorageLike | undefined = globalThis.localStorage): void {
try {
storage?.removeItem(AVATAR_STORAGE_KEY);
} catch {
/* 隐私模式静默 */
}
}
/**
* 取字(顺序判定,先命中先返回):含 "@" 先截 "@" 前段(邮箱取首字母);
* 首字符必须用展开 [...s][0](禁用 charAt(0),避免代理对截半出乱码)。
* ASCII 字母转大写;纯数字回落 ""(剪影);符号/emoji/空白回落 ""(剪影);其余(CJK 等)原样。
*/
export function pickAvatarText(name: string | null | undefined): string {
let s = String(name ?? "");
const at = s.indexOf("@");
if (at > -1) s = s.slice(0, at);
const ch = [...s.trim()][0];
if (ch === undefined) return "";
if (/^[A-Za-z]$/.test(ch)) return ch.toUpperCase();
if (/^[0-9]$/.test(ch)) return "";
if (/^[\p{P}\p{S}\s]$/u.test(ch)) return "";
return ch;
}
+12 -33
View File
@@ -1,5 +1,5 @@
import { IObj, UseTableSetting } from "@/types/Basics"; import { IObj, UseTableSetting } from "@/types/Basics";
import { resolveComponent } from "vue"; import { actionIcon, actionText } from "@/utils/tableActions";
export default class Utils { export default class Utils {
/** /**
* 格式化日期 * 格式化日期
@@ -76,7 +76,8 @@ export default class Utils {
} }
/** /**
* iview Tooltip封装 * iview Tooltip封装(旧 API,薄封装转调 tableActions.actionIcon,签名不变)
* 2026-10-10 起 D1 修正:禁用态 onClick 置 null(原先禁用仍可点)。
*/ */
public static operatingHandle( public static operatingHandle(
h: any, h: any,
@@ -85,30 +86,10 @@ export default class Utils {
callback: Function, callback: Function,
disabled: boolean = false, disabled: boolean = false,
) { ) {
if (disabled) iconClass += " disabled"; return actionIcon(h, { key: info, label: info, icon: iconClass, on: () => callback(), disabled });
return h(
resolveComponent("Tooltip"),
{
content: info,
placement: "bottom",
transfer: true,
},
{
default: () => [
h(
"div",
{
class: `operating-btn iconfont ${iconClass}`,
onClick: callback,
},
null,
),
],
},
);
} }
/** 旧 API,薄封装转调 tableActions.actionText(签名不变;第三参仅识别 text-error → danger) */
public static operatingTextHandle( public static operatingTextHandle(
h: any, h: any,
info: string, info: string,
@@ -116,15 +97,13 @@ export default class Utils {
callback: Function, callback: Function,
disabled: boolean = false, disabled: boolean = false,
) { ) {
if (disabled) className += " disabled"; return actionText(h, {
return h( key: info,
"div", label: info,
{ danger: (className || "").includes("text-error"),
class: `operating-link ${className}`, on: () => callback(),
onClick: disabled ? null : callback, disabled,
}, });
info,
);
} }
/** /**
+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
@@ -433,7 +433,7 @@ onMounted(async () => {
.embed-sec-h > span:first-child { position: relative; padding-left: 9px; } .embed-sec-h > span:first-child { position: relative; padding-left: 9px; }
.embed-sec-h > span:first-child::before { .embed-sec-h > span:first-child::before {
content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%); content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
width: 3px; height: 13px; border-radius: 2px; background: #2d8cf0; width: 3px; height: 13px; border-radius: 2px; background: var(--brand);
} }
/* 两处切换器(视图 / 口径)现同排在标题行,同为 iView small button group、几何与配色一致 /* 两处切换器(视图 / 口径)现同排在标题行,同为 iView small button group、几何与配色一致
(活动态 #0d9488、非活动 #515a6e/#dcdee2)。⚠️ 两者都落在 .embed-sec-h(600) 内 ⇒ 会继承粗体, (活动态 #0d9488、非活动 #515a6e/#dcdee2)。⚠️ 两者都落在 .embed-sec-h(600) 内 ⇒ 会继承粗体,
@@ -462,7 +462,7 @@ onMounted(async () => {
} }
.embed-step.is-done .embed-step-dot { background: #19be6b; box-shadow: 0 0 0 1px #b7ebc9; } .embed-step.is-done .embed-step-dot { background: #19be6b; box-shadow: 0 0 0 1px #b7ebc9; }
.embed-step.is-reject .embed-step-dot { background: #ed4014; box-shadow: 0 0 0 1px #ffd2c9; } .embed-step.is-reject .embed-step-dot { background: #ed4014; box-shadow: 0 0 0 1px #ffd2c9; }
.embed-step.is-active .embed-step-dot { background: #2d8cf0; box-shadow: 0 0 0 3px rgba(45, 140, 240, .18); } .embed-step.is-active .embed-step-dot { background: var(--brand); box-shadow: 0 0 0 3px rgba(45, 140, 240, .18); }
.embed-step-body { flex: 1; min-width: 0; } .embed-step-body { flex: 1; min-width: 0; }
.embed-step-head { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; flex-wrap: wrap; } .embed-step-head { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; flex-wrap: wrap; }
.embed-step-name { font-size: 14px; font-weight: 600; } .embed-step-name { font-size: 14px; font-weight: 600; }
+1 -1
View File
@@ -458,7 +458,7 @@ watch(() => route.query, () => load());
rx="8" rx="8"
:class="{ 'node-active': nodeState(n) === 'active' }" :class="{ 'node-active': nodeState(n) === 'active' }"
:fill="nodeState(n) === 'rejected' ? '#fff1f0' : '#fff'" :fill="nodeState(n) === 'rejected' ? '#fff1f0' : '#fff'"
:stroke="STATE_STROKE[nodeState(n) || ''] || '#2d8cf0'" :style="{ stroke: STATE_STROKE[nodeState(n) || ''] || 'var(--brand)' }"
:stroke-width="nodeState(n) === 'active' ? 2.5 : 1.5" /> :stroke-width="nodeState(n) === 'active' ? 2.5 : 1.5" />
<text :x="pos(n).x + pos(n).w / 2" <text :x="pos(n).x + pos(n).w / 2"
:y="pos(n).y + (nameLines(n).length >= 2 ? 25 : 27)" :y="pos(n).y + (nameLines(n).length >= 2 ? 25 : 27)"
+9 -8
View File
@@ -7,6 +7,7 @@ import MyConfirm from "@/components/MyConfirm";
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue"; import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
import { OperatorEnum } from "@/types"; import { OperatorEnum } from "@/types";
import { flowApi, INSTANCE_STATUS, INSTANCE_STATUS_COLOR } from "@/api/module/flow"; import { flowApi, INSTANCE_STATUS, INSTANCE_STATUS_COLOR } from "@/api/module/flow";
import { actionColumn } from "@/utils/tableActions";
import Utils from "@/utils"; import Utils from "@/utils";
const router = useRouter(); const router = useRouter();
@@ -23,14 +24,14 @@ const columns = [
{ title: "当前节点", key: "currentNodes", width: 150 }, { title: "当前节点", key: "currentNodes", width: 150 },
{ title: "提交时间", width: 170, render: (h: any, p: any) => h("span", (p.row.submitTime || "").replace("T", " ")) }, { title: "提交时间", width: 170, render: (h: any, p: any) => h("span", (p.row.submitTime || "").replace("T", " ")) },
{ title: "办结时间", width: 170, render: (h: any, p: any) => h("span", (p.row.finishTime || "").replace("T", " ") || "-") }, { title: "办结时间", width: 170, render: (h: any, p: any) => h("span", (p.row.finishTime || "").replace("T", " ") || "-") },
{ title: "操作", width: 180, align: "center", fixed: "right", render: (h: any, p: any) => { actionColumn(
const btns = [Utils.operatingHandle(h, "详情", "icon-browse", () => router.push(`/instance/${p.row.instanceId}?pid=101`))]; [
if (p.row.status === 10) { { key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => router.push(`/instance/${r.instanceId}?pid=101`) },
btns.push(Utils.operatingHandle(h, "撤回", "icon-yaohao", () => onWithdraw(p.row))); { key: "withdraw", label: "撤回", icon: "icon-yaohao", on: (r: any) => onWithdraw(r), when: (r: any) => r.status === 10, danger: true },
btns.push(Utils.operatingHandle(h, "催办", "icon-fasong", () => onUrge(p.row))); { key: "urge", label: "催办", icon: "icon-fasong", on: (r: any) => onUrge(r), when: (r: any) => r.status === 10 },
} ],
return btns; { mode: "text", width: 180, fixed: "right" },
} }, ),
]; ];
// dataApi 返回 Method(alova usePagination 负责发送与解包),不能调 .send() // dataApi 返回 Method(alova usePagination 负责发送与解包),不能调 .send()
+4 -6
View File
@@ -6,6 +6,7 @@ import { useRouter } from "vue-router";
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue"; import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
import { OperatorEnum } from "@/types"; import { OperatorEnum } from "@/types";
import { flowApi, TASK_STATUS, TASK_STATUS_COLOR, INSTANCE_STATUS } from "@/api/module/flow"; import { flowApi, TASK_STATUS, TASK_STATUS_COLOR, INSTANCE_STATUS } from "@/api/module/flow";
import { actionColumn } from "@/utils/tableActions";
import Utils from "@/utils"; import Utils from "@/utils";
const router = useRouter(); const router = useRouter();
@@ -22,8 +23,7 @@ const todoColumns = [
{ title: "当前节点", key: "nodeName", width: 160 }, { title: "当前节点", key: "nodeName", width: 160 },
{ title: "提交人", width: 100, render: (h: any, p: any) => h("span", p.row.submitterName || p.row.assigneeName || "-") }, { title: "提交人", width: 100, render: (h: any, p: any) => h("span", p.row.submitterName || p.row.assigneeName || "-") },
{ title: "到达时间", width: 170, render: (h: any, p: any) => h("span", (p.row.arriveTime || "").replace("T", " ")) }, { title: "到达时间", width: 170, render: (h: any, p: any) => h("span", (p.row.arriveTime || "").replace("T", " ")) },
{ title: "操作", width: 100, align: "center", fixed: "right", render: (h: any, p: any) => actionColumn([{ key: "handle", label: "办理", icon: "icon-edit", on: (r: any) => goHandle(r) }], { mode: "text", width: 100, fixed: "right" }),
Utils.operatingHandle(h, "办理", "icon-edit", () => goHandle(p.row)) },
]; ];
const doneColumns = [ const doneColumns = [
@@ -45,8 +45,7 @@ const doneColumns = [
{ title: "意见", key: "opinion", minWidth: 120 }, { title: "意见", key: "opinion", minWidth: 120 },
{ title: "办理时间", width: 170, render: (h: any, p: any) => h("span", (p.row.handleTime || "").replace("T", " ")) }, { title: "办理时间", width: 170, render: (h: any, p: any) => h("span", (p.row.handleTime || "").replace("T", " ")) },
{ title: "办件状态", width: 100, render: (h: any, p: any) => h("span", INSTANCE_STATUS[p.row.instanceStatus] || p.row.instanceStatus) }, { title: "办件状态", width: 100, render: (h: any, p: any) => h("span", INSTANCE_STATUS[p.row.instanceStatus] || p.row.instanceStatus) },
{ title: "操作", width: 100, align: "center", fixed: "right", render: (h: any, p: any) => actionColumn([{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => router.push(`/instance/${r.instanceId}?pid=100`) }], { mode: "text", width: 100, fixed: "right" }),
Utils.operatingHandle(h, "详情", "icon-browse", () => router.push(`/instance/${p.row.instanceId}?pid=100`)) },
]; ];
// 抄送行是 NOTIFY 知会(非任务):无 taskId/status,直接跳办件详情 // 抄送行是 NOTIFY 知会(非任务):无 taskId/status,直接跳办件详情
@@ -56,8 +55,7 @@ const ccColumns = [
{ title: "抄送节点", key: "nodeName", width: 160 }, { title: "抄送节点", key: "nodeName", width: 160 },
{ title: "抄送时间", width: 170, render: (h: any, p: any) => h("span", (p.row.sentTime || "").replace("T", " ")) }, { title: "抄送时间", width: 170, render: (h: any, p: any) => h("span", (p.row.sentTime || "").replace("T", " ")) },
{ title: "办件状态", width: 100, render: (h: any, p: any) => h("span", INSTANCE_STATUS[p.row.instanceStatus] || p.row.instanceStatus || "-") }, { title: "办件状态", width: 100, render: (h: any, p: any) => h("span", INSTANCE_STATUS[p.row.instanceStatus] || p.row.instanceStatus || "-") },
{ title: "操作", width: 100, align: "center", fixed: "right", render: (h: any, p: any) => actionColumn([{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => router.push(`/instance/${r.instanceId}?pid=100`) }], { mode: "text", width: 100, fixed: "right" }),
Utils.operatingHandle(h, "详情", "icon-browse", () => router.push(`/instance/${p.row.instanceId}?pid=100`)) },
]; ];
// dataApi 返回 Method(alova usePagination 负责发送与解包),不能调 .send() // dataApi 返回 Method(alova usePagination 负责发送与解包),不能调 .send()
+11 -16
View File
@@ -7,6 +7,7 @@ import { Message } from "view-ui-plus";
import MyConfirm from "@/components/MyConfirm"; import MyConfirm from "@/components/MyConfirm";
import UserSelectInput from "@/components/UserSelectInput.vue"; import UserSelectInput from "@/components/UserSelectInput.vue";
import { flowApi } from "@/api/module/flow"; import { flowApi } from "@/api/module/flow";
import { actionColumn } from "@/utils/tableActions";
import Utils from "@/utils"; import Utils from "@/utils";
const tab = ref("apps"); const tab = ref("apps");
@@ -91,22 +92,16 @@ const deptColumns = [
h("Tag", { props: { color: p.row.status === 0 ? "green" : "gray" } }, h("Tag", { props: { color: p.row.status === 0 ? "green" : "gray" } },
() => (p.row.status === 0 ? "启用" : "停用")), () => (p.row.status === 0 ? "启用" : "停用")),
}, },
{ actionColumn(
title: "操作", [
width: 260, { key: "members", label: "成员", icon: "icon-yonghu", on: (r: any) => onMembers(r) },
align: "center", { key: "rename", label: "改名", icon: "icon-edit", on: (r: any) => onRename(r) },
render: (h: any, p: any) => [ { key: "disable", label: "停用", icon: "icon-stop", on: (r: any) => onDeptStatus(r, false), when: (r: any) => r.status === 0, danger: true },
Utils.operatingHandle(h, "成员", "icon-yonghu", () => onMembers(p.row)), { key: "enable", label: "启用", icon: "icon-play", on: (r: any) => onDeptStatus(r, true), when: (r: any) => r.status !== 0 },
h("span", { style: "display:inline-block;width:10px" }), { key: "del", label: "删除", icon: "icon-shanchu", on: (r: any) => onDelete(r), danger: true },
Utils.operatingHandle(h, "改名", "icon-edit", () => onRename(p.row)),
h("span", { style: "display:inline-block;width:10px" }),
p.row.status === 0
? Utils.operatingHandle(h, "停用", "icon-stop", () => onDeptStatus(p.row, false))
: Utils.operatingHandle(h, "启用", "icon-play", () => onDeptStatus(p.row, true)),
h("span", { style: "display:inline-block;width:10px" }),
Utils.operatingHandle(h, "删除", "icon-shanchu", () => onDelete(p.row)),
], ],
}, { mode: "text", width: 180 },
),
]; ];
async function loadDepts() { async function loadDepts() {
@@ -361,7 +356,7 @@ onMounted(() => {
<Table :columns="[ <Table :columns="[
{ title: '用户ID', key: 'userId', width: 100 }, { title: '用户ID', key: 'userId', width: 100 },
{ title: '姓名', key: 'userName' }, { title: '姓名', key: 'userName' },
{ title: '操作', width: 90, align: 'center', render: (h, p) => Utils.operatingHandle(h, '移除', 'icon-shanchu', () => onMemberRemove(p.row)) }, actionColumn([{ key: 'remove', label: '移除', icon: 'icon-shanchu', on: (r: any) => onMemberRemove(r), danger: true }], { mode: 'text', width: 90 }),
]" :data="members" :loading="memberLoading" size="small" max-height="360" no-data-text="暂无成员" /> ]" :data="members" :loading="memberLoading" size="small" max-height="360" no-data-text="暂无成员" />
<template #footer> <template #footer>
<Button @click="memberVisible = false">关闭</Button> <Button @click="memberVisible = false">关闭</Button>
+10 -6
View File
@@ -7,6 +7,7 @@ import MyConfirm from "@/components/MyConfirm";
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue"; import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
import { OperatorEnum } from "@/types"; import { OperatorEnum } from "@/types";
import { flowApi } from "@/api/module/flow"; import { flowApi } from "@/api/module/flow";
import { actionColumn } from "@/utils/tableActions";
import Utils from "@/utils"; import Utils from "@/utils";
const LOG_STATUS: Record<number, string> = { 0: "待推/重试中", 1: "成功", 2: "死信" }; const LOG_STATUS: Record<number, string> = { 0: "待推/重试中", 1: "成功", 2: "死信" };
@@ -29,10 +30,14 @@ const regColumns = [
{ title: "回调地址", key: "callbackUrl", minWidth: 240 }, { title: "回调地址", key: "callbackUrl", minWidth: 240 },
{ title: "订阅事件", key: "eventTypes", width: 200, render: (h: any, p: any) => h("span", p.row.eventTypes || "全部") }, { title: "订阅事件", key: "eventTypes", width: 200, render: (h: any, p: any) => h("span", p.row.eventTypes || "全部") },
{ title: "状态", width: 80, align: "center", render: (h: any, p: any) => h("Tag", { color: p.row.status === 0 ? "green" : "gray" }, () => (p.row.status === 0 ? "启用" : "停用")) }, { title: "状态", width: 80, align: "center", render: (h: any, p: any) => h("Tag", { color: p.row.status === 0 ? "green" : "gray" }, () => (p.row.status === 0 ? "启用" : "停用")) },
{ title: "操作", width: 140, align: "center", fixed: "right", render: (h: any, p: any) => [ actionColumn(
Utils.operatingHandle(h, p.row.status === 0 ? "停用" : "启用", p.row.status === 0 ? "icon-stop" : "icon-shiyong", () => onRegStatus(p.row, p.row.status === 0)), [
Utils.operatingHandle(h, "重置secret", "icon-shezhi", () => onResetSecret(p.row)), { key: "disable", label: "停用", icon: "icon-stop", on: (r: any) => onRegStatus(r, true), when: (r: any) => r.status === 0, danger: true },
] }, { key: "enable", label: "启用", icon: "icon-shiyong", on: (r: any) => onRegStatus(r, false), when: (r: any) => r.status !== 0 },
{ key: "resetSecret", label: "重置secret", icon: "icon-shezhi", on: (r: any) => onResetSecret(r) },
],
{ mode: "text", width: 140, fixed: "right" },
),
]; ];
// ---- 推送日志 ---- // ---- 推送日志 ----
@@ -64,8 +69,7 @@ const hungColumns = [
{ title: "重试", key: "retryCount", width: 70, align: "center", render: (h: any, p: any) => h("Tag", { color: "orange" }, () => `${p.row.retryCount} 次`) }, { title: "重试", key: "retryCount", width: 70, align: "center", render: (h: any, p: any) => h("Tag", { color: "orange" }, () => `${p.row.retryCount} 次`) },
{ title: "末次结果", key: "lastError", minWidth: 220, render: (h: any, p: any) => h("span", p.row.lastError || p.row.lastHttpStatus || "-") }, { title: "末次结果", key: "lastError", minWidth: 220, render: (h: any, p: any) => h("span", p.row.lastError || p.row.lastHttpStatus || "-") },
{ title: "下次重推", width: 160, render: (h: any, p: any) => h("span", (p.row.nextRetryTime || "").replace("T", " ") || "-") }, { title: "下次重推", width: 160, render: (h: any, p: any) => h("span", (p.row.nextRetryTime || "").replace("T", " ") || "-") },
{ title: "操作", width: 120, align: "center", fixed: "right", render: (h: any, p: any) => actionColumn([{ key: "markDead", label: "标记死信", icon: "icon-ashbin", on: (r: any) => onMarkDead(r), danger: true }], { mode: "text", width: 120, fixed: "right" }),
Utils.operatingHandle(h, "标记死信", "icon-ashbin", () => onMarkDead(p.row)) },
]; ];
async function loadRegs() { async function loadRegs() {
+17 -18
View File
@@ -8,6 +8,7 @@ import { Message } from "view-ui-plus";
import MyConfirm from "@/components/MyConfirm"; import MyConfirm from "@/components/MyConfirm";
import SourceDrawer from "@/views/flow/components/SourceDrawer.vue"; import SourceDrawer from "@/views/flow/components/SourceDrawer.vue";
import { flowApi } from "@/api/module/flow"; import { flowApi } from "@/api/module/flow";
import { actionColumn } from "@/utils/tableActions";
import Utils from "@/utils"; import Utils from "@/utils";
const router = useRouter(); const router = useRouter();
@@ -40,26 +41,24 @@ const columns = [
} }, } },
{ title: "GUID", key: "flowGuid", width: 200 }, { title: "GUID", key: "flowGuid", width: 200 },
{ title: "发布时间", width: 160, render: (h: any, p: any) => h("span", (p.row.publishTime || "").replace("T", " ") || "-") }, { title: "发布时间", width: 160, render: (h: any, p: any) => h("span", (p.row.publishTime || "").replace("T", " ") || "-") },
{ title: "操作", width: 310, align: "center", fixed: "right", render: (h: any, p: any) => { actionColumn(
const btns: any[] = []; [
if (p.row.status === 0) { { key: "edit", label: "编辑", icon: "icon-edit", on: (r: any) => onEdit(r), when: (r: any) => r.status === 0 },
btns.push(Utils.operatingHandle(h, "编辑", "icon-edit", () => onEdit(p.row))); { key: "publish", label: "发布", icon: "icon-fasong", on: (r: any) => onPublish(r), when: (r: any) => r.status === 0 },
btns.push(Utils.operatingHandle(h, "发布", "icon-fasong", () => onPublish(p.row))); { key: "newVersion", label: "升版本", icon: "icon-tianjia", on: (r: any) => onNewVersion(r), when: (r: any) => r.status === 1 },
} else if (p.row.status === 1) { // 停用:破坏性动作(已发布→停用),danger 沉底并进「更多」
btns.push(Utils.operatingHandle(h, "升版本", "icon-tianjia", () => onNewVersion(p.row))); { key: "offline", label: "停用", icon: "icon-stop", on: (r: any) => onOffline(r), when: (r: any) => r.status === 1, danger: true },
btns.push(Utils.operatingHandle(h, "停用", "icon-stop", () => onOffline(p.row)));
} else if (p.row.status === 2) {
// 与「停用」对称的专用启用通道(后端 /def/enable:仅停用版→已发布,重跑发布校验; // 与「停用」对称的专用启用通道(后端 /def/enable:仅停用版→已发布,重跑发布校验;
// 草稿不走这里,草稿用「发布」) // 草稿不走这里,草稿用「发布」)
btns.push(Utils.operatingHandle(h, "启用", "icon-play", () => onEnable(p.row))); { key: "enable", label: "启用", icon: "icon-play", on: (r: any) => onEnable(r), when: (r: any) => r.status === 2 },
} // 元数据原地编辑(2026-10-10 议题2):改名称/归属应用不升版本(含已发布/停用)
// 元数据原地编辑(2026-10-10 议题2):改名称/归属应用不升版本(含已发布/停用) { key: "editMeta", label: "编辑信息", icon: "icon-bianji", on: (r: any) => onEditMeta(r) },
btns.push(Utils.operatingHandle(h, "编辑信息", "icon-bianji", () => onEditMeta(p.row))); { key: "diagram", label: "图示", icon: "icon-tupian", on: (r: any) => onDiagram(r) },
btns.push(Utils.operatingHandle(h, "图示", "icon-tupian", () => onDiagram(p.row))); { key: "source", label: "源码", icon: "icon-liebiao", on: (r: any) => onSource(r) },
btns.push(Utils.operatingHandle(h, "源码", "icon-liebiao", () => onSource(p.row))); { key: "versions", label: "版本历史", icon: "icon-shijian", on: (r: any) => onVersions(r) },
btns.push(Utils.operatingHandle(h, "版本历史", "icon-shijian", () => onVersions(p.row))); ],
return btns; { mode: "text", width: 180, fixed: "right" },
} }, ),
]; ];
async function load() { async function load() {
+9 -8
View File
@@ -7,6 +7,7 @@ import MyConfirm from "@/components/MyConfirm";
import { OperatorEnum } from "@/types"; import { OperatorEnum } from "@/types";
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue"; import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
import { flowApi, INSTANCE_STATUS, INSTANCE_STATUS_COLOR } from "@/api/module/flow"; import { flowApi, INSTANCE_STATUS, INSTANCE_STATUS_COLOR } from "@/api/module/flow";
import { actionColumn } from "@/utils/tableActions";
import Utils from "@/utils"; import Utils from "@/utils";
const router = useRouter(); const router = useRouter();
@@ -22,14 +23,14 @@ const columns = [
h("Tag", { color: INSTANCE_STATUS_COLOR[p.row.status] || "gray" }, () => INSTANCE_STATUS[p.row.status] || p.row.status) }, h("Tag", { color: INSTANCE_STATUS_COLOR[p.row.status] || "gray" }, () => INSTANCE_STATUS[p.row.status] || p.row.status) },
{ title: "提交人", key: "submitterName", width: 100 }, { title: "提交人", key: "submitterName", width: 100 },
{ title: "提交时间", width: 160, render: (h: any, p: any) => h("span", (p.row.submitTime || "").replace("T", " ")) }, { title: "提交时间", width: 160, render: (h: any, p: any) => h("span", (p.row.submitTime || "").replace("T", " ")) },
{ title: "操作", width: 170, align: "center", fixed: "right", render: (h: any, p: any) => [ actionColumn(
Utils.operatingHandle(h, "详情", "icon-browse", () => router.push(`/instance/${p.row.id}?pid=102`)), [
h("span", { style: "display:inline-block;width:10px" }), { key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => router.push(`/instance/${r.id}?pid=102`) },
// 仅审批中的办件可能挂在条件分支节点(重跑入口对非挂起办件会得到业务错提示) // 仅审批中的办件可能挂在条件分支节点(重跑入口对非挂起办件会得到业务错提示)
p.row.status === 10 { key: "reroute", label: "重跑路由", icon: "icon-play", on: (r: any) => onReroute(r), when: (r: any) => r.status === 10 },
? Utils.operatingHandle(h, "重跑路由", "icon-play", () => onReroute(p.row)) ],
: h("span"), { mode: "text", width: 170, fixed: "right" },
] }, ),
]; ];
/** 重跑路由(2026-09-24 §4.4):重新调判断服务,成功即按出口续走;仍失败回业务错(办件继续挂起) */ /** 重跑路由(2026-09-24 §4.4):重新调判断服务,成功即按出口续走;仍失败回业务错(办件继续挂起) */
+8 -12
View File
@@ -6,6 +6,7 @@ import { ref, onMounted } from "vue";
import { Message } from "view-ui-plus"; import { Message } from "view-ui-plus";
import MyConfirm from "@/components/MyConfirm"; import MyConfirm from "@/components/MyConfirm";
import { flowApi } from "@/api/module/flow"; import { flowApi } from "@/api/module/flow";
import { actionColumn } from "@/utils/tableActions";
import Utils from "@/utils"; import Utils from "@/utils";
const loading = ref(false); const loading = ref(false);
@@ -30,19 +31,14 @@ const columns = [
render: (h: any, p: any) => render: (h: any, p: any) =>
h("Tag", { color: p.row.status === 1 ? "green" : "gray" }, () => (p.row.status === 1 ? "启用" : "停用")), h("Tag", { color: p.row.status === 1 ? "green" : "gray" }, () => (p.row.status === 1 ? "启用" : "停用")),
}, },
{ actionColumn(
title: "操作", [
width: 150, { key: "edit", label: "编辑", icon: "icon-edit", on: (r: any) => onEdit(r) },
align: "center", { key: "disable", label: "停用", icon: "icon-stop", on: (r: any) => onDisable(r), when: (r: any) => r.status === 1, danger: true },
fixed: "right", { key: "enable", label: "启用", icon: "icon-play", on: (r: any) => onEnable(r), when: (r: any) => r.status !== 1 },
render: (h: any, p: any) => [
Utils.operatingHandle(h, "编辑", "icon-edit", () => onEdit(p.row)),
h("span", { style: "display:inline-block;width:10px" }),
p.row.status === 1
? Utils.operatingHandle(h, "停用", "icon-stop", () => onDisable(p.row))
: Utils.operatingHandle(h, "启用", "icon-play", () => onEnable(p.row)),
], ],
}, { mode: "text", width: 150, fixed: "right" },
),
]; ];
async function load() { async function load() {
@@ -55,7 +55,7 @@ function onOpen(e: MouseEvent): void {
transition: all 0.15s; transition: all 0.15s;
} }
.node-card:hover { .node-card:hover {
border-color: #2d8cf0; border-color: var(--brand);
} }
.node-card.error { .node-card.error {
border-color: #ed4014; border-color: #ed4014;
@@ -72,7 +72,7 @@ function onOpen(e: MouseEvent): void {
height: 26px; height: 26px;
border-radius: 6px; border-radius: 6px;
background: #e8f3ff; background: #e8f3ff;
color: #2d8cf0; color: var(--brand);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -90,7 +90,7 @@ function renameOutlet(i: number, v: string): void {
} }
.ol-title { .ol-title {
text-align: center; text-align: center;
color: #2d8cf0; color: var(--brand);
font-weight: 600; font-weight: 600;
font-size: 13px; font-size: 13px;
cursor: pointer; cursor: pointer;
@@ -139,7 +139,7 @@ function renameOutlet(i: number, v: string): void {
} }
.ol-col-no { .ol-col-no {
flex: none; flex: none;
color: #2d8cf0; color: var(--brand);
font-weight: 600; font-weight: 600;
font-size: 13px; font-size: 13px;
} }
@@ -150,12 +150,12 @@ function renameOutlet(i: number, v: string): void {
border-radius: 6px; border-radius: 6px;
padding: 2px 8px; padding: 2px 8px;
font-size: 12px; font-size: 12px;
color: #2d8cf0; color: var(--brand);
font-weight: 600; font-weight: 600;
} }
.ol-outlet:focus { .ol-outlet:focus {
outline: none; outline: none;
border-color: #2d8cf0; border-color: var(--brand);
} }
.ol-col-del { .ol-col-del {
flex: none; flex: none;
@@ -170,8 +170,8 @@ function renameOutlet(i: number, v: string): void {
.ol-add { .ol-add {
margin-top: 12px; margin-top: 12px;
border-style: dashed; border-style: dashed;
color: #2d8cf0; color: var(--brand);
border-color: #2d8cf0; border-color: var(--brand);
background: transparent; background: transparent;
width: auto; width: auto;
padding: 0 16px; padding: 0 16px;
@@ -182,7 +182,7 @@ function renameOutlet(i: number, v: string): void {
.ol-add:hover { .ol-add:hover {
background: #f0faff; background: #f0faff;
color: #2b85e4; color: #2b85e4;
border-color: #2d8cf0; border-color: var(--brand);
} }
.ol-issue { .ol-issue {
text-align: center; text-align: center;
@@ -75,8 +75,8 @@ function pick(kind: "approval" | "parallel" | "notify" | "condition"): void {
height: 22px; height: 22px;
border-radius: 50%; border-radius: 50%;
background: #fff; background: #fff;
border: 1px solid #2d8cf0; border: 1px solid var(--brand);
color: #2d8cf0; color: var(--brand);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -90,7 +90,7 @@ function pick(kind: "approval" | "parallel" | "notify" | "condition"): void {
} }
.plus-wrap:hover .plus-btn { .plus-wrap:hover .plus-btn {
opacity: 1; opacity: 1;
background: #2d8cf0; background: var(--brand);
color: #fff; color: #fff;
box-shadow: 0 0 0 4px rgba(45, 140, 240, 0.15); box-shadow: 0 0 0 4px rgba(45, 140, 240, 0.15);
} }
@@ -117,7 +117,7 @@ function pick(kind: "approval" | "parallel" | "notify" | "condition"): void {
font-size: 12px; font-size: 12px;
} }
.mi-icon.approval { .mi-icon.approval {
background: #2d8cf0; background: var(--brand);
} }
.mi-icon.notify { .mi-icon.notify {
background: #13a8a8; background: #13a8a8;
+2 -2
View File
@@ -536,7 +536,7 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
/* 撑满布局壳可视区:100vh − 56(顶栏) − 56(面包屑 .path-nav) − 24(main-container 下内边距) /* 撑满布局壳可视区:100vh − 56(顶栏) − 56(面包屑 .path-nav) − 24(main-container 下内边距)
(实测值,见 layout.css .header-container/.path-nav/.base-wrap-main .main-container) */ (实测值,见通道 base.layout.css .header-container/.path-nav/.base-wrap-main .main-container) */
height: calc(100vh - 136px); height: calc(100vh - 136px);
background: #f5f7fa; background: #f5f7fa;
} }
@@ -562,7 +562,7 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
font-size: 18px; font-size: 18px;
} }
.hd-back:hover { .hd-back:hover {
color: #2d8cf0; color: var(--brand);
} }
.hd-title { .hd-title {
font-size: 15px; font-size: 15px;
+2 -2
View File
@@ -129,9 +129,9 @@ function fmtTime(v: string | null | undefined): string {
padding: 2px 8px; padding: 2px 8px;
display: inline-block; display: inline-block;
font-size: 12px; font-size: 12px;
color: #2d8cf0; color: var(--brand);
background: #f0f7ff; background: #f0f7ff;
border-left: 3px solid #2d8cf0; border-left: 3px solid var(--brand);
border-radius: 2px; border-radius: 2px;
} }
.line { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; } .line { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; }