- A 类主题 loader 原文接入 + env-config 引导 + vite /static 与 /api(8086) 代理(P6/P11)
- GetUserMod 左导航:utils/menuNav.ts 单点实现 path 空锚点跳过、分组收敛、icon 兜底、面包屑基线(对齐 Notice 13afb22 refined 版)
- alova 封装 + api/module/base.ts(getUserMod 可选 appCode,P2 定名;H4 前空树不回落,P3)
- 路由骨架一 SPA 四形态:/admin/{forms,data,stats,dicts}、/f/:code、/app/:appCode/*、/my;self!=top 剥壳(方案 §3.3)
- 规范件落位:tableActions.ts(UserBase 同构源,正文 MD5 不变)、TablePageAlovaNew + useElementHeight、page-wrapper yh-fill 骨架
- view-ui-plus 精确锁 1.3.24;本地零 layout.css/icons.css;avatar 两件由 YschTheme sync 下发
- 剥除:notice 领域 views/api/mock/menu 兜底、alova-vant(mobiapi)、ztree+jquery 与重依赖(按需回补,见 service/docs/T5-report.md 问题单 P52)
@@ -0,0 +1,14 @@
|
||||
# 开发环境 (dev):仅 vite dev --mode dev 加载;build 产物不读任何 .env.*
|
||||
# (线上由 /api/env-config?target=front 运行时下发,见 index.html 引导脚本注释)。
|
||||
# ⚠️ VITE_* 仅本地开发生效。
|
||||
# API 走 /api 同源相对路径(vite proxy 转发到本地后端 8086,执行手册 §5 P6),
|
||||
# 规避后端 CAS 过滤器不回 CORS 头导致的跨域拦截;SSO/基础平台用真实环境
|
||||
VITE_BASE_HOST=http://base.yunhaixingtong.com
|
||||
VITE_SSO_HOST=http://sso.yunhaixingtong.com
|
||||
VITE_SERVICE_HOST=
|
||||
VITE_IS_MOCK=false
|
||||
# 主题通道本地联调(规范 §2.2 挂载步):dev 下 index.html 的 loader 走同源相对 /static/…,
|
||||
# 由 vite.config 的 /static 代理兜到中央通道(默认线上通道域)。
|
||||
# 联调本机通道源:YschTheme 侧 npm run serve:static(默认 9527),再起 dev 前设
|
||||
# YH_THEME_PROXY_TARGET=http://localhost:9527
|
||||
# 覆盖该 target;通道 CORS 打通后也可改用 VITE_THEME_HOST 跨源直连。
|
||||
@@ -0,0 +1,8 @@
|
||||
# 本地纯前端调试模式:仅 vite dev --mode mock 加载(build 产物不读任何 .env.*,
|
||||
# 线上由 /api/env-config?target=front 运行时下发)。后端 SSO/base 不可达时启用内置 mock
|
||||
# (src/assets/mock 的模块树,用于壳与左导航冒烟,见 local-debug/t5-shell-smoke.sh)
|
||||
# 启动:npm run dev:mock
|
||||
VITE_BASE_HOST=
|
||||
VITE_SSO_HOST=
|
||||
VITE_SERVICE_HOST=
|
||||
VITE_IS_MOCK=true
|
||||
@@ -1,12 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="../../favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>表单中心</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 运行时配置引导:在任何应用模块执行前同步拉取 /<contextPath>/api/env-config
|
||||
(后端免登录端点,projman 配置管理在线维护),替代生产构建环境变量。
|
||||
必须同步:host.config.ts 在模块求值期就读 window.__APP_CONFIG__;失败/本地开发
|
||||
(根路径,段数不足 2)则不设置:本地走 .env.dev/.env.mock(VITE_*),线上走同域推导兜底。
|
||||
target = URL 第 2 段目录名:本应用产物部署于 /{contextPath}/front/,故为 front,
|
||||
对应后端配置键 yh.env-config.source.front(环境变量 YH_ENV_CONFIG_SOURCE_FRONT)。
|
||||
注意:同步 XHR 不支持 timeout 属性(赋值会抛 InvalidAccessError),依赖内网快速失败 -->
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var seg = location.pathname.split("/").filter(Boolean);
|
||||
if (seg.length < 2) return; // 本地开发(根路径):走 .env.dev / .env.mock
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", "/" + seg[0] + "/api/env-config?target=" + encodeURIComponent(seg[1]), false);
|
||||
xhr.send(null);
|
||||
if (xhr.status === 200) {
|
||||
var conf = JSON.parse(xhr.responseText);
|
||||
if (conf && typeof conf === "object") window.__APP_CONFIG__ = conf;
|
||||
}
|
||||
} catch (e) {
|
||||
/* 拉取失败保持未设置,走同域推导兜底 */
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<!-- 主题通道引导(A 类免插件接入,见《云海子应用前端接入规范》§2.1,原文照抄勿改):
|
||||
唯一接入点。不装 npm 依赖、不改构建配置;FOUC 门、manifest 预取、主题 <link> 追加与
|
||||
排序(晚于 viewuiplus.css)全由通道 yh-theme-init.js 承担,主题升级走 /static 通道,
|
||||
本应用无需重新构建。
|
||||
origin 零写死,优先级四档:__APP_CONFIG__.staticHost(专用通道覆盖)
|
||||
> __APP_CONFIG__.baseHost(env-config 在线维护,换域名改配置即生效)
|
||||
> %VITE_THEME_HOST%(仅 dev 注入的跨源直连,需通道已开 CORS)
|
||||
> 同源相对 "/static/…"(生产常态:本应用部署在 base 域 /<app>/front/,与通道同源;
|
||||
本地 dev 由 vite.config 的 /static 代理兜到通道)。
|
||||
document.write 是有意为之:保证 parser-blocking,让档位类与 FOUC 门先于首帧;
|
||||
build 不读 .env,占位符留字面量 → 判首字符 % 置空 → 产物零环境值烧入。 -->
|
||||
<script>
|
||||
(function () {
|
||||
var c = window.__APP_CONFIG__ || {};
|
||||
var themeHost = "%VITE_THEME_HOST%";
|
||||
if (themeHost.charAt(0) === "%") themeHost = "";
|
||||
var host = String(c.staticHost || c.baseHost || themeHost).replace(/\/+$/, "");
|
||||
document.write('<script src="' + host + '/static/js/yh-theme-init.js"><\/script>');
|
||||
})();
|
||||
</script>
|
||||
<title><%- title %></title>
|
||||
<%- injectScript %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -1,22 +1,39 @@
|
||||
{
|
||||
"name": "form-center-front",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"nodeversion": "20.19.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"typescript": "5.2.2",
|
||||
"vite": "^5.1.4",
|
||||
"vue-tsc": "1.8.27"
|
||||
}
|
||||
"name": "form-center-front",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"nodeversion": "20.19.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --mode dev",
|
||||
"dev:mock": "vite --mode mock",
|
||||
"build": "vite build --mode base",
|
||||
"preview": "vite preview",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"alova": "^3.4.0",
|
||||
"mitt": "^3.0.1",
|
||||
"mockjs": "^1.1.0",
|
||||
"pinia": "^2.1.4",
|
||||
"sass": "^1.64.1",
|
||||
"view-ui-plus": "1.3.24",
|
||||
"vite-plugin-mock": "2.9.6",
|
||||
"vue": "^3.4.19",
|
||||
"vue-router": "4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.4.4",
|
||||
"@unocss/preset-rem-to-px": "^66.6.0",
|
||||
"@unocss/preset-uno": "^66.6.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"typescript": "^5.2.2",
|
||||
"unocss": "^66.6.0",
|
||||
"unplugin-auto-import": "0.18.3",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vue-tsc": "^1.8.27"
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -1,15 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||
import layout from "./components/layout.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import config from "@/assets/config/host.config";
|
||||
import { baseApi } from "@/api/module/base";
|
||||
import { resolveMenuPage } from "@/utils/menuRoute";
|
||||
import { buildNavTree, firstNavPage, isSingleNav, type NavNode } from "@/utils/menuNav";
|
||||
import { Message } from "view-ui-plus";
|
||||
import type { IModule } from "@/types/Basics";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const isReady = ref(false);
|
||||
|
||||
/**
|
||||
* 页面即组件(方案 §3.3):被 iframe 嵌入时自动剥壳——不渲染顶栏/左导航/面包屑,
|
||||
* 也不发两个鉴权请求(嵌入上下文里本应用 /api/base/* 取不到会话会弹错误提示并画出带壳页面)。
|
||||
* 显式 mode 参数(?mode=embed|page)的闭集判定在 T7 填报页落地,壳这层只兜 self != top 这一条。
|
||||
*/
|
||||
const isEmbedded = window.self !== window.top;
|
||||
|
||||
/**
|
||||
* 嵌入态必须旁路全局 body 的 min-width(assets/styles/base/_base.scss 的 1200px):
|
||||
* isEmbedded 只旁路了组件壳,管不到全局 CSS,窄 iframe 里仍按 ≥1200px 排版(整体右偏 + 右裁)。
|
||||
* 内联样式优先级高于样式表;非嵌入态移除内联属性回落原值。
|
||||
*/
|
||||
function syncEmbedLayout(embedded: boolean) {
|
||||
if (embedded) document.body.style.minWidth = "0";
|
||||
else document.body.style.removeProperty("min-width");
|
||||
}
|
||||
|
||||
const layoutData = reactive({
|
||||
appName: "表单中心",
|
||||
baseHost: config.baseHost,
|
||||
ssoHost: config.ssoHost,
|
||||
userInfo: null as any,
|
||||
/** 已按「path 空=权限锚点不进导航」规则过滤的可渲染菜单树(见 utils/menuNav.ts) */
|
||||
navList: [] as NavNode[],
|
||||
isAlone: false,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
// 首次导航异步完成后再判默认跳转:mount 时 currentRoute 可能仍是初始 "/"
|
||||
await router.isReady();
|
||||
syncEmbedLayout(isEmbedded);
|
||||
isReady.value = true;
|
||||
if (isEmbedded) return;
|
||||
|
||||
try {
|
||||
// 1. 当前会话用户
|
||||
const user: any = await baseApi.getUserInfo().send();
|
||||
const { id, name, profilePicture } = user || {};
|
||||
|
||||
// 2. 当前用户已授权的模块树(左导航数据源;path 空的权限锚点行也在内,由导航层跳过)
|
||||
const modList: IModule[] = (await baseApi.getUserMod().send()) || [];
|
||||
layoutData.userInfo = { id, name: name || "用户", avatar: profilePicture || "" };
|
||||
layoutData.navList = buildNavTree(modList, resolveMenuPage);
|
||||
layoutData.isAlone = isSingleNav(layoutData.navList);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// 冒烟取证:原始树 vs 可渲染导航(path 空的权限锚点应当只出现在前者)
|
||||
console.table(
|
||||
(modList || []).map((m: any) => ({
|
||||
name: m?.name,
|
||||
id: m?.id,
|
||||
modId: m?.modId,
|
||||
page: m?.page,
|
||||
icon: m?.icon,
|
||||
children: (m?.children || []).length
|
||||
}))
|
||||
);
|
||||
console.table(
|
||||
layoutData.navList.map((n) => ({ 渲染项: n.name, page: n.page, 分组头: n.group }))
|
||||
);
|
||||
}
|
||||
|
||||
// 入口页:跳第一个可跳转菜单项(无授权用户即「我的填写」)
|
||||
if (router.currentRoute.value.path === "/") {
|
||||
const first = firstNavPage(layoutData.navList);
|
||||
if (first) router.replace(first);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("加载用户信息失败", error);
|
||||
if (error?.message !== "redirect") {
|
||||
Message.error("载入用户信息失败,请检查后端服务是否正常!");
|
||||
}
|
||||
// 降级只降「导航为空」,不造兜底菜单树——静默回落会把「平台未配模块/未授权」伪装成正常
|
||||
layoutData.userInfo = { name: "用户", avatar: "" };
|
||||
layoutData.navList = [];
|
||||
layoutData.isAlone = true;
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => syncEmbedLayout(false));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="shell-placeholder">
|
||||
<h1>表单中心</h1>
|
||||
<p>工程壳已就绪(T1)。管理端框架、设计器与填报页在 T5~T8 落地。</p>
|
||||
</main>
|
||||
<!-- 嵌入态:裸渲染,页面自己管理样式与滚动(方案 §3.3 剥壳) -->
|
||||
<router-view v-if="isEmbedded" :key="route.fullPath" />
|
||||
<template v-else>
|
||||
<layout v-if="isReady" :layoutData="layoutData" />
|
||||
<div v-if="isReady" class="base-wrap-main" :class="{ 'alone-page': layoutData.isAlone }">
|
||||
<div class="main-wrapper">
|
||||
<!-- key=fullPath:仅 query 变化的深链(如详情页换 id)强制重渲染,避免显示上一条数据 -->
|
||||
<router-view :key="route.fullPath" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped>
|
||||
.shell-placeholder {
|
||||
font-family: system-ui, -apple-system, "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
<style lang="scss">
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
/* 页面级滚动容器:main-wrapper 定高 + 纵向滚动。内容超一屏时若只给 min-height,
|
||||
会被 body 的 overflow:hidden 直接裁掉且无滚动条;列表页高度由 .table-region 实测(规范 §4.3) */
|
||||
.base-wrap-main .main-wrapper {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
/* 表格操作列(operatingHandle/actionIcon 渲染的 Tooltip 包裹层)横排且随单元格居中 */
|
||||
.ivu-table-cell > .ivu-tooltip {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: 0 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { alovaInstance } from "@/utils/alova/alova-iview";
|
||||
import type { IModule } from "@/types/Basics";
|
||||
|
||||
/**
|
||||
* 基础平台接口(notice/front 的 api/module 形态 + flow-center base.ts 同构):
|
||||
* 用户信息与已授权模块列表(左导航数据源)。
|
||||
*
|
||||
* 模块行在 UserBase 按本应用(app_code=form-center)配置,path 须逐字对上
|
||||
* router/routes.ts(手册 §2.2 表);<b>path 空的行是纯权限锚点</b>(如 customId
|
||||
* fc:scope:all),只由后端 DataScopeService 扫描授权集合消费,前端按 utils/menuNav.ts
|
||||
* 的规则跳过渲染——接口本身不过滤,前端拿到的仍是全量授权树。
|
||||
*
|
||||
* 写法与 Notice 的一处差异:Notice 用 `new Method<T>(...)`,而 alova 3.5 的 Method 第一个
|
||||
* 泛型位是 AlovaGenerics(不是响应类型),strict vue-tsc 下报 TS2344;本端改用实例方法
|
||||
* `alovaInstance.Get<T>()` 拿同一个 Method 对象、走同一套拦截器与信封解包,行为等价。
|
||||
*/
|
||||
const BASE = "/api/base";
|
||||
|
||||
class BaseApi {
|
||||
/** 当前登录用户信息 */
|
||||
getUserInfo() {
|
||||
return alovaInstance.Get(`${BASE}/GetUserInfo`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户已授权的模块树(左侧菜单数据源)。
|
||||
*
|
||||
* @param appCode 可选目标应用(快应用上下文 /app/{appCode}/ 要的是<b>那个应用</b>的树,
|
||||
* 参数名按执行手册 §5 P2 定稿 appCode)。UserBase 侧 H4 未上线时后端
|
||||
* 返回空树并打 warn——前端据此显示空导航,<b>不得回落本应用树</b>(P3 裁决)。
|
||||
*/
|
||||
getUserMod(appCode?: string) {
|
||||
return alovaInstance.Get<IModule[]>(`${BASE}/GetUserMod`, {
|
||||
params: appCode ? { appCode } : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const baseApi = new BaseApi();
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { BaseConf } from "@/types/Basics";
|
||||
|
||||
/**
|
||||
* 运行时配置(fork notice/front 的 host.config.ts,剥 notice 领域的 flowHost/relayConf)。
|
||||
*
|
||||
* 配置优先级(高 → 低):
|
||||
* ① window.__APP_CONFIG__:线上运行时配置,index.html 引导脚本在应用模块加载前同步拉取
|
||||
* /<contextPath>/api/env-config?target=front(后端免登录端点,projman 配置管理在线维护)写入;
|
||||
* 主题通道的 origin 也由该对象提供(loader 读 staticHost / baseHost)。
|
||||
* ② import.meta.env.VITE_*:仅本地开发/演示(vite dev --mode dev 读 .env.dev、--mode mock 读 .env.mock);
|
||||
* build 产物不读任何 .env.*,本层自动跳过,产物不含任何环境值。
|
||||
* ③ 同域推导兜底:env-config 未配置/拉取失败时框架至少能渲染并调通同源 API;
|
||||
* ssoHost 为空 → 登出跳转不可用——与后端未就绪状态一致,可接受。
|
||||
*/
|
||||
const env = import.meta.env;
|
||||
|
||||
// ③ 同域推导兜底:context 段取当前路径首段(/{contextPath}/front/ → contextPath,即后端上下文),
|
||||
// 与服务端 context-path 对齐(本地无、线上 /form-center);本地 dev/mock 在根路径运行(无段),
|
||||
// serviceHost 留空走 vite 同源代理(规避 CAS 过滤器不回 CORS 头,见 vite.config 的 /api 代理)
|
||||
const seg = window.location.pathname.split("/").filter(Boolean);
|
||||
const origin = window.location.origin;
|
||||
|
||||
const conf: BaseConf = {
|
||||
baseHost: origin,
|
||||
ssoHost: "",
|
||||
serviceHost: seg.length >= 2 ? `${origin}/${seg[0]}` : "",
|
||||
isMock: false,
|
||||
isDebug: env.DEV
|
||||
};
|
||||
|
||||
// 逐键覆盖:只覆盖来源里实际出现且非空的键,未出现的键沿用底层值
|
||||
const applyConf = (target: Record<string, any>, source: any) => {
|
||||
if (!source || typeof source !== "object") return;
|
||||
Object.keys(source).forEach((key) => {
|
||||
if (source[key] !== undefined && source[key] !== null && source[key] !== "") {
|
||||
target[key] = source[key];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ② 本地开发/演示(build 产物中恒 undefined → 自动跳过)
|
||||
applyConf(conf, {
|
||||
baseHost: env.VITE_BASE_HOST,
|
||||
ssoHost: env.VITE_SSO_HOST,
|
||||
serviceHost: env.VITE_IS_MOCK === "true" ? "" : env.VITE_SERVICE_HOST,
|
||||
isMock: env.VITE_IS_MOCK === "true"
|
||||
});
|
||||
|
||||
// ① 线上运行时配置(最高优先,覆盖前面所有)
|
||||
applyConf(conf, (window as any).__APP_CONFIG__);
|
||||
|
||||
// HTTPS 自动升级:当前页面是 https 时,将所有 http:// 配置项升级为 https://
|
||||
if (document.location.protocol === "https:") {
|
||||
Object.keys(conf).forEach((key) => {
|
||||
const value = conf[key];
|
||||
if (typeof value === "string" && value.indexOf("http://") > -1) {
|
||||
conf[key] = value.replace(/http:\/\//g, "https://");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default conf;
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M41.65 64.5995C42.942 65.8915 45.05 66.2995 46.75 65.4495C49.742 64.1575 52.292 62.8995 54.842 60.7575C56.134 59.4655 56.984 57.7995 56.542 55.6575C56.1 53.1075 56.542 50.1155 57.834 47.5655C59.126 45.0155 61.642 43.3155 64.226 42.4655C65.926 42.0235 67.184 40.3235 67.626 38.6575C68.068 35.6655 68.068 32.2655 67.626 29.3075C67.184 27.6075 65.926 25.9075 64.226 25.4655C61.676 24.6155 59.126 22.9155 57.834 20.3655C56.542 17.8155 56.134 14.8235 56.542 12.2735C56.984 10.5735 56.1 8.43153 54.842 7.17353C52.292 5.06553 49.742 3.77353 46.75 2.48153C45.05 1.63153 42.942 2.03953 41.65 3.33153C37.4 7.24153 30.6 7.24153 26.35 3.39953C25.058 2.10753 22.95 1.69953 21.25 2.54953C18.292 3.80753 15.708 5.09953 13.158 7.24153C11.458 8.53353 11.016 10.2335 11.458 12.3415C12.75 17.8495 9.35 23.7995 3.842 25.4995C1.7 25.9415 0.442 27.6415 0.442 29.3415C0 31.0415 0 32.2995 0 33.9995C0 35.6995 0 36.9915 0.442 38.6915C0.442 40.3915 1.734 42.0915 3.842 42.4995C6.392 43.3495 8.942 45.0495 10.234 47.5995C11.526 50.1495 11.934 53.1415 11.492 55.6915C11.05 57.3915 11.934 59.4995 13.192 60.7915C15.742 62.9335 18.292 64.1915 21.284 65.4835C22.984 66.3335 25.126 65.9255 26.384 64.6335C30.6 60.3495 37.4 60.3495 41.65 64.5995Z" fill="#919BF2"/>
|
||||
<path d="M24.208 34C24.208 35.2859 24.4613 36.5592 24.9534 37.7472C25.4455 38.9353 26.1667 40.0147 27.076 40.924C27.9853 41.8333 29.0647 42.5545 30.2528 43.0466C31.4408 43.5387 32.7141 43.792 34 43.792C35.2859 43.792 36.5592 43.5387 37.7472 43.0466C38.9353 42.5545 40.0147 41.8333 40.924 40.924C41.8333 40.0147 42.5545 38.9353 43.0466 37.7472C43.5387 36.5592 43.792 35.2859 43.792 34C43.792 32.7141 43.5387 31.4408 43.0466 30.2528C42.5545 29.0648 41.8333 27.9853 40.924 27.076C40.0147 26.1667 38.9353 25.4455 37.7472 24.9534C36.5592 24.4613 35.2859 24.208 34 24.208C32.7141 24.208 31.4408 24.4613 30.2528 24.9534C29.0647 25.4455 27.9853 26.1667 27.076 27.076C26.1667 27.9853 25.4455 29.0648 24.9534 30.2528C24.4613 31.4408 24.208 32.7141 24.208 34Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7245_84837)">
|
||||
<path d="M52.5096 4.53333H43.443V2.26667C43.443 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2942 0 23.043 0C21.7918 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70484 4.53333 2.64297 8.5952 2.64297 13.6V54.4C2.64297 59.4048 6.70484 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.519 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.243 48.8512 16.243 47.6C16.243 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9654 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.243 35.2512 16.243 34C16.243 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9654 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.243 21.6512 16.243 20.4C16.243 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9654 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M58.5962 56.782C57.2642 55.486 56.1722 54.142 55.3202 52.75C54.2762 54.346 52.8962 55.678 51.1802 56.746L50.4602 55.576C52.2602 54.532 53.6462 53.182 54.6182 51.526C53.8502 50.062 53.2622 48.478 52.8542 46.774C52.5182 47.434 52.1162 48.064 51.6482 48.664L50.8742 47.692C52.1462 45.856 53.0102 43.384 53.4662 40.276L54.7442 40.492C54.6122 41.332 54.4382 42.184 54.2222 43.048H58.8482V44.29H57.7322C57.6482 47.122 57.0722 49.564 56.0042 51.616C56.9162 53.164 58.0442 54.508 59.3882 55.648L58.5962 56.782ZM53.8622 44.29L53.5022 45.28C53.9342 47.128 54.5222 48.796 55.2662 50.284C56.0342 48.532 56.4542 46.534 56.5262 44.29H53.8622ZM43.8542 42.004H46.2122V40.402H47.4542V42.004H49.3802V43.156H47.4542V44.992H48.4982C49.4342 43.804 50.2562 42.502 50.9642 41.086L51.9722 41.608C51.3122 42.856 50.6282 43.984 49.9202 44.992H51.2522V46.198H49.0022C48.4862 46.81 47.9942 47.35 47.5262 47.818H50.5862V48.754C49.9742 49.474 49.2542 50.194 48.4262 50.914V51.796C49.5422 51.628 50.6102 51.406 51.6302 51.13V52.336C50.6942 52.588 49.6262 52.798 48.4262 52.966V55.054C48.4262 56.098 47.9162 56.62 46.8962 56.62H45.0962L44.7722 55.45C45.5282 55.51 46.0682 55.54 46.3922 55.54C46.9082 55.54 47.1662 55.264 47.1662 54.712V53.146C45.8342 53.302 44.5142 53.44 43.2062 53.56L43.0262 52.318C44.3342 52.258 45.7142 52.144 47.1662 51.976V50.662C47.8382 50.158 48.4682 49.594 49.0562 48.97H46.2662C45.3182 49.75 44.3882 50.392 43.4762 50.896L42.6662 49.978C43.1942 49.678 43.7342 49.342 44.2862 48.97H43.8722V47.818H45.7802C46.3682 47.326 46.9322 46.786 47.4722 46.198H43.0082V44.992H46.2122V43.156H43.8542V42.004Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7245_84837">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7245_84837)">
|
||||
<path d="M52.5096 4.53333H43.443V2.26667C43.443 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2942 0 23.043 0C21.7918 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70484 4.53333 2.64297 8.5952 2.64297 13.6V54.4C2.64297 59.4048 6.70484 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.519 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.243 48.8512 16.243 47.6C16.243 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9654 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.243 35.2512 16.243 34C16.243 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9654 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.243 21.6512 16.243 20.4C16.243 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9654 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M58.5962 56.782C57.2642 55.486 56.1722 54.142 55.3202 52.75C54.2762 54.346 52.8962 55.678 51.1802 56.746L50.4602 55.576C52.2602 54.532 53.6462 53.182 54.6182 51.526C53.8502 50.062 53.2622 48.478 52.8542 46.774C52.5182 47.434 52.1162 48.064 51.6482 48.664L50.8742 47.692C52.1462 45.856 53.0102 43.384 53.4662 40.276L54.7442 40.492C54.6122 41.332 54.4382 42.184 54.2222 43.048H58.8482V44.29H57.7322C57.6482 47.122 57.0722 49.564 56.0042 51.616C56.9162 53.164 58.0442 54.508 59.3882 55.648L58.5962 56.782ZM53.8622 44.29L53.5022 45.28C53.9342 47.128 54.5222 48.796 55.2662 50.284C56.0342 48.532 56.4542 46.534 56.5262 44.29H53.8622ZM43.8542 42.004H46.2122V40.402H47.4542V42.004H49.3802V43.156H47.4542V44.992H48.4982C49.4342 43.804 50.2562 42.502 50.9642 41.086L51.9722 41.608C51.3122 42.856 50.6282 43.984 49.9202 44.992H51.2522V46.198H49.0022C48.4862 46.81 47.9942 47.35 47.5262 47.818H50.5862V48.754C49.9742 49.474 49.2542 50.194 48.4262 50.914V51.796C49.5422 51.628 50.6102 51.406 51.6302 51.13V52.336C50.6942 52.588 49.6262 52.798 48.4262 52.966V55.054C48.4262 56.098 47.9162 56.62 46.8962 56.62H45.0962L44.7722 55.45C45.5282 55.51 46.0682 55.54 46.3922 55.54C46.9082 55.54 47.1662 55.264 47.1662 54.712V53.146C45.8342 53.302 44.5142 53.44 43.2062 53.56L43.0262 52.318C44.3342 52.258 45.7142 52.144 47.1662 51.976V50.662C47.8382 50.158 48.4682 49.594 49.0562 48.97H46.2662C45.3182 49.75 44.3882 50.392 43.4762 50.896L42.6662 49.978C43.1942 49.678 43.7342 49.342 44.2862 48.97H43.8722V47.818H45.7802C46.3682 47.326 46.9322 46.786 47.4722 46.198H43.0082V44.992H46.2122V43.156H43.8542V42.004Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7245_84837">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7245_84837)">
|
||||
<path d="M52.5096 4.53333H43.443V2.26667C43.443 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2942 0 23.043 0C21.7918 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70484 4.53333 2.64297 8.5952 2.64297 13.6V54.4C2.64297 59.4048 6.70484 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.519 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.243 48.8512 16.243 47.6C16.243 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9654 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.243 35.2512 16.243 34C16.243 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9654 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.243 21.6512 16.243 20.4C16.243 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9654 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M58.5962 56.782C57.2642 55.486 56.1722 54.142 55.3202 52.75C54.2762 54.346 52.8962 55.678 51.1802 56.746L50.4602 55.576C52.2602 54.532 53.6462 53.182 54.6182 51.526C53.8502 50.062 53.2622 48.478 52.8542 46.774C52.5182 47.434 52.1162 48.064 51.6482 48.664L50.8742 47.692C52.1462 45.856 53.0102 43.384 53.4662 40.276L54.7442 40.492C54.6122 41.332 54.4382 42.184 54.2222 43.048H58.8482V44.29H57.7322C57.6482 47.122 57.0722 49.564 56.0042 51.616C56.9162 53.164 58.0442 54.508 59.3882 55.648L58.5962 56.782ZM53.8622 44.29L53.5022 45.28C53.9342 47.128 54.5222 48.796 55.2662 50.284C56.0342 48.532 56.4542 46.534 56.5262 44.29H53.8622ZM43.8542 42.004H46.2122V40.402H47.4542V42.004H49.3802V43.156H47.4542V44.992H48.4982C49.4342 43.804 50.2562 42.502 50.9642 41.086L51.9722 41.608C51.3122 42.856 50.6282 43.984 49.9202 44.992H51.2522V46.198H49.0022C48.4862 46.81 47.9942 47.35 47.5262 47.818H50.5862V48.754C49.9742 49.474 49.2542 50.194 48.4262 50.914V51.796C49.5422 51.628 50.6102 51.406 51.6302 51.13V52.336C50.6942 52.588 49.6262 52.798 48.4262 52.966V55.054C48.4262 56.098 47.9162 56.62 46.8962 56.62H45.0962L44.7722 55.45C45.5282 55.51 46.0682 55.54 46.3922 55.54C46.9082 55.54 47.1662 55.264 47.1662 54.712V53.146C45.8342 53.302 44.5142 53.44 43.2062 53.56L43.0262 52.318C44.3342 52.258 45.7142 52.144 47.1662 51.976V50.662C47.8382 50.158 48.4682 49.594 49.0562 48.97H46.2662C45.3182 49.75 44.3882 50.392 43.4762 50.896L42.6662 49.978C43.1942 49.678 43.7342 49.342 44.2862 48.97H43.8722V47.818H45.7802C46.3682 47.326 46.9322 46.786 47.4722 46.198H43.0082V44.992H46.2122V43.156H43.8542V42.004Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7245_84837">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7245_84837)">
|
||||
<path d="M52.5096 4.53333H43.443V2.26667C43.443 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2942 0 23.043 0C21.7918 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70484 4.53333 2.64297 8.5952 2.64297 13.6V54.4C2.64297 59.4048 6.70484 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.519 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.243 48.8512 16.243 47.6C16.243 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9654 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.243 35.2512 16.243 34C16.243 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9654 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.243 21.6512 16.243 20.4C16.243 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9654 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M58.5962 56.782C57.2642 55.486 56.1722 54.142 55.3202 52.75C54.2762 54.346 52.8962 55.678 51.1802 56.746L50.4602 55.576C52.2602 54.532 53.6462 53.182 54.6182 51.526C53.8502 50.062 53.2622 48.478 52.8542 46.774C52.5182 47.434 52.1162 48.064 51.6482 48.664L50.8742 47.692C52.1462 45.856 53.0102 43.384 53.4662 40.276L54.7442 40.492C54.6122 41.332 54.4382 42.184 54.2222 43.048H58.8482V44.29H57.7322C57.6482 47.122 57.0722 49.564 56.0042 51.616C56.9162 53.164 58.0442 54.508 59.3882 55.648L58.5962 56.782ZM53.8622 44.29L53.5022 45.28C53.9342 47.128 54.5222 48.796 55.2662 50.284C56.0342 48.532 56.4542 46.534 56.5262 44.29H53.8622ZM43.8542 42.004H46.2122V40.402H47.4542V42.004H49.3802V43.156H47.4542V44.992H48.4982C49.4342 43.804 50.2562 42.502 50.9642 41.086L51.9722 41.608C51.3122 42.856 50.6282 43.984 49.9202 44.992H51.2522V46.198H49.0022C48.4862 46.81 47.9942 47.35 47.5262 47.818H50.5862V48.754C49.9742 49.474 49.2542 50.194 48.4262 50.914V51.796C49.5422 51.628 50.6102 51.406 51.6302 51.13V52.336C50.6942 52.588 49.6262 52.798 48.4262 52.966V55.054C48.4262 56.098 47.9162 56.62 46.8962 56.62H45.0962L44.7722 55.45C45.5282 55.51 46.0682 55.54 46.3922 55.54C46.9082 55.54 47.1662 55.264 47.1662 54.712V53.146C45.8342 53.302 44.5142 53.44 43.2062 53.56L43.0262 52.318C44.3342 52.258 45.7142 52.144 47.1662 51.976V50.662C47.8382 50.158 48.4682 49.594 49.0562 48.97H46.2662C45.3182 49.75 44.3882 50.392 43.4762 50.896L42.6662 49.978C43.1942 49.678 43.7342 49.342 44.2862 48.97H43.8722V47.818H45.7802C46.3682 47.326 46.9322 46.786 47.4722 46.198H43.0082V44.992H46.2122V43.156H43.8542V42.004Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7245_84837">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7245_84837)">
|
||||
<path d="M52.5096 4.53333H43.443V2.26667C43.443 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2942 0 23.043 0C21.7918 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70484 4.53333 2.64297 8.5952 2.64297 13.6V54.4C2.64297 59.4048 6.70484 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.519 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.243 48.8512 16.243 47.6C16.243 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9654 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.243 35.2512 16.243 34C16.243 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9654 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.243 21.6512 16.243 20.4C16.243 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9654 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M58.5962 56.782C57.2642 55.486 56.1722 54.142 55.3202 52.75C54.2762 54.346 52.8962 55.678 51.1802 56.746L50.4602 55.576C52.2602 54.532 53.6462 53.182 54.6182 51.526C53.8502 50.062 53.2622 48.478 52.8542 46.774C52.5182 47.434 52.1162 48.064 51.6482 48.664L50.8742 47.692C52.1462 45.856 53.0102 43.384 53.4662 40.276L54.7442 40.492C54.6122 41.332 54.4382 42.184 54.2222 43.048H58.8482V44.29H57.7322C57.6482 47.122 57.0722 49.564 56.0042 51.616C56.9162 53.164 58.0442 54.508 59.3882 55.648L58.5962 56.782ZM53.8622 44.29L53.5022 45.28C53.9342 47.128 54.5222 48.796 55.2662 50.284C56.0342 48.532 56.4542 46.534 56.5262 44.29H53.8622ZM43.8542 42.004H46.2122V40.402H47.4542V42.004H49.3802V43.156H47.4542V44.992H48.4982C49.4342 43.804 50.2562 42.502 50.9642 41.086L51.9722 41.608C51.3122 42.856 50.6282 43.984 49.9202 44.992H51.2522V46.198H49.0022C48.4862 46.81 47.9942 47.35 47.5262 47.818H50.5862V48.754C49.9742 49.474 49.2542 50.194 48.4262 50.914V51.796C49.5422 51.628 50.6102 51.406 51.6302 51.13V52.336C50.6942 52.588 49.6262 52.798 48.4262 52.966V55.054C48.4262 56.098 47.9162 56.62 46.8962 56.62H45.0962L44.7722 55.45C45.5282 55.51 46.0682 55.54 46.3922 55.54C46.9082 55.54 47.1662 55.264 47.1662 54.712V53.146C45.8342 53.302 44.5142 53.44 43.2062 53.56L43.0262 52.318C44.3342 52.258 45.7142 52.144 47.1662 51.976V50.662C47.8382 50.158 48.4682 49.594 49.0562 48.97H46.2662C45.3182 49.75 44.3882 50.392 43.4762 50.896L42.6662 49.978C43.1942 49.678 43.7342 49.342 44.2862 48.97H43.8722V47.818H45.7802C46.3682 47.326 46.9322 46.786 47.4722 46.198H43.0082V44.992H46.2122V43.156H43.8542V42.004Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7245_84837">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7186_97095)">
|
||||
<path d="M52.5096 4.53333H43.443V2.26667C43.443 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2942 0 23.043 0C21.7918 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70484 4.53333 2.64297 8.5952 2.64297 13.6V54.4C2.64297 59.4048 6.70484 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.519 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.243 48.8512 16.243 47.6C16.243 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9654 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.243 35.2512 16.243 34C16.243 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9654 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.243 21.6512 16.243 20.4C16.243 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9654 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M44.1242 40.672C45.2522 41.032 46.2482 41.452 47.1122 41.932L46.4282 42.976C45.4922 42.424 44.5142 41.98 43.4942 41.644L44.1242 40.672ZM51.1982 40.402C51.0062 40.93 50.8502 41.314 50.7302 41.554H57.9842V42.328C57.7682 43.228 57.4802 44.104 57.1202 44.956L55.8782 44.578C56.1662 44.05 56.4242 43.42 56.6522 42.688H53.5922C53.5202 43.12 53.4122 43.516 53.2682 43.876C54.2042 45.256 56.0582 46.21 58.8302 46.738L58.3622 47.89C55.7222 47.35 53.8502 46.366 52.7462 44.938C52.5182 45.286 52.2782 45.592 52.0262 45.856C51.1262 46.696 49.6802 47.35 47.6882 47.818L47.2022 46.666C48.9782 46.282 50.2562 45.754 51.0362 45.082C51.6962 44.47 52.1342 43.672 52.3502 42.688H50.0462C49.5422 43.336 48.9482 43.93 48.2642 44.47L47.5082 43.462C48.6722 42.61 49.4762 41.536 49.9202 40.24L51.1982 40.402ZM56.7602 48.304V53.65H55.4642V49.51H46.4822V53.794H45.1862V48.304H56.7602ZM51.7562 50.302C51.5042 52.582 50.9582 54.118 50.1182 54.91C49.1702 55.894 47.0822 56.512 43.8542 56.764L43.3502 55.612C46.2182 55.444 48.1082 54.952 49.0202 54.136C49.8362 53.356 50.3162 52.042 50.4602 50.194L51.7562 50.302ZM47.3822 44.938C46.4822 45.874 45.3662 46.756 44.0342 47.584L43.1702 46.576C44.5262 45.808 45.6782 44.968 46.6262 44.056L47.3822 44.938ZM52.1702 53.596C54.4982 54.208 56.6282 54.946 58.5602 55.81L57.8582 56.89C55.7822 55.906 53.6642 55.138 51.5042 54.586L52.1702 53.596Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7186_97095">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7186_97102)">
|
||||
<path d="M52.5096 4.53333H43.4429V2.26667C43.4429 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2941 0 23.0429 0C21.7917 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70481 4.53333 2.64294 8.5952 2.64294 13.6V54.4C2.64294 59.4048 6.70481 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.5189 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.2429 48.8512 16.2429 47.6C16.2429 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9653 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.2429 35.2512 16.2429 34C16.2429 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9653 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.2429 21.6512 16.2429 20.4C16.2429 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9653 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M46.2482 45.172H47.4902V47.962H49.3082V49.15H47.4902V51.67C48.1742 51.526 48.8222 51.382 49.4342 51.238V52.444L47.4902 52.894V56.818H46.2482V53.128C45.2402 53.308 44.1362 53.47 42.9362 53.614L42.7742 52.354C44.2382 52.186 45.3962 52.03 46.2482 51.886V49.15H43.2602L42.9902 48.016C43.5062 46.888 43.9862 45.484 44.4302 43.804H42.8282V42.562H44.7362C44.9402 41.65 45.0962 40.894 45.2042 40.294L46.4282 40.492L45.9962 42.562H49.2542V43.804H45.6902C45.2702 45.424 44.8142 46.81 44.3222 47.962H46.2482V45.172ZM49.8302 42.58H53.6822V40.366H54.9062V42.58H57.3002C56.9042 42.04 56.4182 41.464 55.8422 40.852L56.7062 40.312C57.3662 41.008 57.8762 41.608 58.2362 42.112L57.5702 42.58H59.2082V43.804H54.9062V45.46H58.5962V55.234C58.5962 56.218 58.1462 56.71 57.2462 56.71H56.0042L55.6802 55.486L56.8322 55.54C57.2042 55.54 57.3902 55.354 57.3902 54.982V52.84H54.9062V56.674H53.6822V52.84H51.3242V56.782H50.1182V45.46H53.6822V43.804H49.8302V42.58ZM53.6822 46.612H51.3242V48.646H53.6822V46.612ZM51.3242 49.762V51.724H53.6822V49.762H51.3242ZM57.3902 51.724V49.762H54.9062V51.724H57.3902ZM57.3902 48.646V46.612H54.9062V48.646H57.3902Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7186_97102">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7186_97108)">
|
||||
<path d="M52.5096 4.53333H43.4429V2.26667C43.4429 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2941 0 23.0429 0C21.7917 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70481 4.53333 2.64294 8.5952 2.64294 13.6V54.4C2.64294 59.4048 6.70481 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.5189 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.2429 48.8512 16.2429 47.6C16.2429 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9653 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.2429 35.2512 16.2429 34C16.2429 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9653 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.2429 21.6512 16.2429 20.4C16.2429 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9653 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M55.2302 55.288C56.7302 55.288 58.0802 55.264 59.2802 55.216L59.0642 56.512H55.5182C51.2102 56.512 48.5162 56.278 47.4362 55.81C46.5002 55.414 45.6542 54.688 44.8982 53.632C44.5742 54.784 44.1242 55.846 43.5482 56.818L42.6122 55.99C43.5722 54.358 44.1002 52.516 44.1962 50.464L45.3482 50.608C45.3122 51.256 45.2522 51.826 45.1682 52.318C45.6362 53.05 46.1522 53.626 46.7162 54.046V49.672H43.1522V48.484H50.7842V49.672H47.8682V51.58H50.3342V52.732H47.8682V54.694C47.9762 54.73 48.0662 54.766 48.1382 54.802C49.1822 55.126 51.5462 55.288 55.2302 55.288ZM43.9622 40.996H50.1002V47.278H43.9622V40.996ZM48.8402 46.198V44.668H45.2222V46.198H48.8402ZM45.2222 43.66H48.8402V42.076H45.2222V43.66ZM51.0362 41.014H58.8122V42.202H55.5722C55.5122 42.73 55.4342 43.21 55.3382 43.642H58.3082V50.914H57.1022V44.758H52.7462V50.914H51.5942V43.642H54.1682C54.2642 43.234 54.3422 42.754 54.4022 42.202H51.0362V41.014ZM56.2202 50.968C57.3362 51.832 58.2722 52.696 59.0282 53.56L58.1102 54.46C57.4142 53.596 56.4962 52.708 55.3562 51.796L56.2202 50.968ZM54.3302 45.604H55.5182V48.142C55.4822 49.834 55.1522 51.202 54.5282 52.246C53.9282 53.182 52.9322 53.926 51.5402 54.478L50.8382 53.362C52.1222 52.894 53.0282 52.252 53.5562 51.436C54.0362 50.572 54.2942 49.474 54.3302 48.142V45.604Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7186_97108">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7186_97114)">
|
||||
<path d="M52.5096 4.53333H43.4429V2.26667C43.4429 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2941 0 23.0429 0C21.7917 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70481 4.53333 2.64294 8.5952 2.64294 13.6V54.4C2.64294 59.4048 6.70481 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.5189 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.2429 48.8512 16.2429 47.6C16.2429 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9653 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.2429 35.2512 16.2429 34C16.2429 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9653 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.2429 21.6512 16.2429 20.4C16.2429 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9653 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M50.4602 48.826H48.7682V47.62H53.4302V48.826H51.7382V53.704C52.5902 53.512 53.3702 53.314 54.0782 53.11V54.316C52.5182 54.76 50.8322 55.138 49.0202 55.45L48.6962 54.226C49.3082 54.154 49.8962 54.064 50.4602 53.956V48.826ZM57.2822 56.53C56.6582 56.53 56.1122 56.062 55.6442 55.126C55.1042 54.046 54.7082 52.576 54.4562 50.716C54.2642 49.192 54.1322 47.218 54.0602 44.794H48.3542V43.534H54.0242C54.0002 42.838 53.9882 41.74 53.9882 40.24H55.2482C55.2482 41.752 55.2602 42.85 55.2842 43.534H58.8662V44.794H55.3022C55.3862 47.83 55.5782 50.218 55.8782 51.958C56.2742 53.998 56.7422 55.054 57.2822 55.126C57.6542 55.054 58.0202 53.95 58.3802 51.814L59.4062 52.642C58.8182 55.234 58.1102 56.53 57.2822 56.53ZM56.7962 40.222C57.5522 41.062 58.1522 41.818 58.5962 42.49L57.6962 43.12C57.2762 42.472 56.6702 41.692 55.8782 40.78L56.7962 40.222ZM44.9162 40.474C46.0442 41.374 47.0102 42.298 47.8142 43.246L46.9322 44.146C46.2002 43.246 45.2282 42.316 44.0162 41.356L44.9162 40.474ZM42.7562 45.802H46.5362V53.65C46.9682 53.23 47.5202 52.654 48.1922 51.922L48.5342 53.29C47.5262 54.382 46.4762 55.336 45.3842 56.152L44.8802 55C45.1442 54.772 45.2762 54.526 45.2762 54.262V47.044H42.7562V45.802Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7186_97114">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7186_97120)">
|
||||
<path d="M52.5096 4.53333H43.4429V2.26667C43.4429 1.01547 42.4275 0 41.1763 0C39.9251 0 38.9096 1.01547 38.9096 2.26667V4.53333H25.3096V2.26667C25.3096 1.01547 24.2941 0 23.0429 0C21.7917 0 20.7763 1.01547 20.7763 2.26667V4.53333H11.7096C6.70481 4.53333 2.64294 8.5952 2.64294 13.6V54.4C2.64294 59.4048 6.70481 63.4667 11.7096 63.4667H52.5096C57.5144 63.4667 61.5763 59.4048 61.5763 54.4V13.6C61.5763 8.5952 57.5189 4.53333 52.5096 4.53333ZM45.7096 49.8667H18.5096C17.2584 49.8667 16.2429 48.8512 16.2429 47.6C16.2429 46.3488 17.2584 45.3333 18.5096 45.3333H45.7096C46.9608 45.3333 47.9763 46.3488 47.9763 47.6C47.9763 48.8512 46.9653 49.8667 45.7096 49.8667ZM45.7096 36.2667H18.5096C17.2584 36.2667 16.2429 35.2512 16.2429 34C16.2429 32.7488 17.2584 31.7333 18.5096 31.7333H45.7096C46.9608 31.7333 47.9763 32.7488 47.9763 34C47.9763 35.2512 46.9653 36.2667 45.7096 36.2667ZM45.7096 22.6667H18.5096C17.2584 22.6667 16.2429 21.6512 16.2429 20.4C16.2429 19.1488 17.2584 18.1333 18.5096 18.1333H45.7096C46.9608 18.1333 47.9763 19.1488 47.9763 20.4C47.9763 21.6512 46.9653 22.6667 45.7096 22.6667Z" fill="#A0CDF2"/>
|
||||
<circle cx="51.5" cy="48.5" r="18.5" fill="white"/>
|
||||
<circle cx="51.6914" cy="48.6912" r="14.6912" fill="#A0CDF2"/>
|
||||
<path d="M42.9181 45.784H48.0661C48.2221 45.196 48.4081 44.452 48.6241 43.552H44.1421V42.346H48.8941C49.0021 41.854 49.1521 41.128 49.3441 40.168L50.6761 40.276L50.2621 42.346H58.0201V43.552H49.9921C49.8241 44.296 49.6441 45.04 49.4521 45.784H59.0821V47.044H49.1101C48.9421 47.62 48.6841 48.412 48.3361 49.42H56.9041V50.536C56.0161 51.904 54.6601 53.242 52.8361 54.55C53.8321 54.91 54.8701 55.348 55.9501 55.864L55.2301 56.998C52.5661 55.678 49.7821 54.628 46.8781 53.848L47.5621 52.84C48.7621 53.164 50.0581 53.566 51.4501 54.046C53.0941 52.966 54.4021 51.826 55.3741 50.626H46.4821C46.9501 49.366 47.3581 48.172 47.7061 47.044H42.9181V45.784Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7186_97120">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
t="1615860353141"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="6624"
|
||||
width="14"
|
||||
height="14"
|
||||
>
|
||||
<path
|
||||
d="M0 102.4h307.2v307.2H0zM0 614.4h307.2v307.2H0zM409.6 102.4h614.4v102.4H409.6zM409.6 307.2h614.4v102.4H409.6zM409.6 614.4h614.4v102.4H409.6zM409.6 819.2h614.4v102.4H409.6z"
|
||||
p-id="6625"
|
||||
fill="#656667"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 914 B |
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<path d="M832 192H544l-64-64H192c-35.2 0-64 28.8-64 64v576c0 35.2 28.8 64 64 64h640c35.2 0 64-28.8 64-64V256c0-35.2-28.8-64-64-64z m0 576H192V192h266.7l64 64H832v512z" fill="#999999"/>
|
||||
<path d="M256 320h512v64H256z m0 128h384v64H256z m0 128h256v64H256z" fill="#cccccc"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 380 B |
@@ -0,0 +1,13 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1" y="1" width="30" height="30" rx="3" fill="white" stroke="#DCDEE0" stroke-width="2" stroke-dasharray="4 4"/>
|
||||
<g clip-path="url(#clip0_125_1092)">
|
||||
<path d="M24 8H8V24H24V8Z" fill="white" fill-opacity="0.01"/>
|
||||
<path d="M16.0209 11.3333L16.0087 20.6666" stroke="#666666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.3333 16H20.6667" stroke="#666666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_125_1092">
|
||||
<rect width="16" height="16" fill="white" transform="translate(8 8)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 695 B |
@@ -0,0 +1,15 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1" y="1" width="30" height="30" rx="3" fill="white" stroke="#DCDEE0" stroke-width="2"/>
|
||||
<g clip-path="url(#clip0_125_1099)">
|
||||
<path d="M24 8H8V24H24V8Z" fill="white" fill-opacity="0.01"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.6667 13H21.3333L20.3333 22.6667H11.6667L10.6667 13Z" stroke="#F15B43" stroke-width="1.125" stroke-linejoin="round"/>
|
||||
<path d="M14.6667 16.3333V19.6667" stroke="#F15B43" stroke-width="1.125" stroke-linecap="round"/>
|
||||
<path d="M17.3333 16.3333V19.6658" stroke="#F15B43" stroke-width="1.125" stroke-linecap="round"/>
|
||||
<path d="M12 13L17.4414 9L20 13" stroke="#F15B43" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_125_1099">
|
||||
<rect width="16" height="16" fill="white" transform="translate(8 8)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 919 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M859.977 336.128H608.251V171.489c0-41.159-33.318-74.836-74.039-74.836h-44.421c-40.729 0-74.044 33.677-74.044 74.836v164.639H164.023c-16.289 0-29.616 13.473-29.616 29.936v149.669c0 16.469 13.326 29.942 29.616 29.942h14.803v231.241c0 65.859-14.803 120.493-14.803 120.493 0 16.459 13.324 29.938 29.611 29.938h636.729c16.291 0 29.615-13.481 29.615-29.938 0 0-14.81-56.882-14.81-123.486V545.675h14.81c16.287 0 29.616-13.477 29.616-29.938V366.064c-0.001-16.469-13.323-29.936-29.617-29.936zM682.282 897.403V699.089c0-5.989-5.18-11.225-11.103-11.225h-7.407c-5.917 0-11.103 5.236-11.103 11.225v198.314H534.212V699.089c0-5.989-5.181-11.225-11.116-11.225H515.7c-5.918 0-11.106 5.236-11.106 11.225v198.314H378.731V699.089c0-5.989-5.186-11.225-11.111-11.225h-7.399c-5.918 0-11.106 5.236-11.106 11.225v198.314H193.634s14.812-53.884 14.812-120.487V545.675h607.113v231.241c0 66.606 14.804 120.487 14.804 120.487H682.282z m177.695-396.634c0 8.233-6.66 14.968-14.81 14.968H178.826c-8.145 0-14.807-6.735-14.807-14.968V381.028c0-8.235 6.662-14.964 14.807-14.964H445.36V178.971c0-29.184 22.956-52.382 51.835-52.382h29.61c28.876 0 51.828 23.199 51.828 52.382v187.093h266.535c8.15 0 14.81 6.729 14.81 14.964v119.741z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<path d="M482.214544 543.934407l-0.099952-59.970717 59.970718-0.099952 0.099951 59.970718-59.970717 0.099951z m119.941435-0.199902l-0.099952-59.970718 59.970718-0.099951 0.099951 59.970717-59.970717 0.099952z m119.941434-0.199903l-0.099951-59.970717 59.970718-0.099951 0.099951 59.970717-59.970718 0.099951z m119.941435-0.299853l-0.099951-59.970718 59.970717-0.099951 0.099952 59.970717-59.970718 0.099952z m119.941435-0.199903l-0.099951-59.970717 59.970717-0.099951 0.099951 59.970717-59.970717 0.099951z" />
|
||||
<path d="M482.914202 1024l-0.099951-59.970717 59.970717-0.099952 0.099951 59.970718-59.970717 0.099951z m-0.199902-119.941435l-0.099952-59.970717 59.970718-0.099951 0.099951 59.970717-59.970717 0.099951z m-0.199903-119.941435l-0.099951-59.970717 59.970717-0.099951 0.099952 59.970717-59.970718 0.099951z m-0.199902-119.941435l-0.099951-59.970717 59.970717-0.099951 0.099951 59.970717-59.970717 0.099951z m-0.099951-119.941434l-0.099952-59.970718 59.970718-0.099951 0.099951 59.970717-59.970717 0.099952z m-0.199903-119.941435l-0.099951-59.970718 59.970718-0.099951 0.099951 59.970718-59.970718 0.099951z m-0.199902-119.941435l-0.099951-59.970718 59.970717-0.099951 0.099951 59.970718-59.970717 0.099951z m-0.199902-119.941435l-0.099952-59.970717 59.970718-0.099952 0.099951 59.970718-59.970717 0.099951z m-0.199903-119.941435l-0.099951-59.970717 59.970717-0.099951 0.099952 59.970717-59.970718 0.099951z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7225_67996)">
|
||||
<path d="M18 0H0V18H18V0Z" fill="white" fill-opacity="0.01"/>
|
||||
<path d="M14.625 2.25H3.375C2.75368 2.25 2.25 2.75368 2.25 3.375V14.625C2.25 15.2463 2.75368 15.75 3.375 15.75H14.625C15.2463 15.75 15.75 15.2463 15.75 14.625V3.375C15.75 2.75368 15.2463 2.25 14.625 2.25Z" stroke="#5B6871" stroke-width="1.125" stroke-linejoin="round"/>
|
||||
<path d="M9 6V12" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 9H12" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7225_67996">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 793 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 0H0V18H18V0Z" fill="white" fill-opacity="0.01"/>
|
||||
<path d="M2.625 15.75H16.125" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.125 10.02V12.75H6.86895L14.625 4.99054L11.8857 2.25L4.125 10.02Z" stroke="#5B6871" stroke-width="1.125" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 424 B |
@@ -0,0 +1,14 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7225_67984)">
|
||||
<path d="M18 0H0V18H18V0Z" fill="white" fill-opacity="0.01"/>
|
||||
<path d="M7 3V15" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 7L7 3" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 15V3" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 15L15 11" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7225_67984">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 740 B |
@@ -0,0 +1 @@
|
||||
<svg t="1784779423851" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6101" width="24" height="24"><path d="M1016.32 494.08l-143.872-143.36c-9.728-9.728-26.112-9.728-35.84 0-9.728 9.728-9.728 26.112 0 35.84L936.96 486.4h-399.36V87.04l99.84 100.352c9.728 9.728 26.112 9.728 35.84 0 9.728-9.728 9.728-26.112 0-35.84l-143.36-143.872c-9.728-9.728-26.112-9.728-35.84 0l-143.36 143.872c-9.728 9.728-9.728 26.112 0 35.84 9.728 9.728 26.112 9.728 35.84 0L486.4 87.04v399.36H87.04l100.352-99.84c9.728-9.728 9.728-26.112 0-35.84-9.728-9.728-26.112-9.728-35.84 0l-143.872 143.36c-9.728 9.728-9.728 26.112 0 35.84l143.872 143.36c9.728 9.728 26.112 9.728 35.84 0 9.728-9.728 9.728-26.112 0-35.84L87.04 537.6h399.36v399.36l-99.84-100.352c-9.728-9.728-26.112-9.728-35.84 0-9.728 9.728-9.728 26.112 0 35.84l143.36 143.872c9.728 9.728 26.112 9.728 35.84 0l143.36-143.872c9.728-9.728 9.728-26.112 0-35.84-9.728-9.728-26.112-9.728-35.84 0L537.6 936.96v-399.36h399.36l-100.352 99.84c-9.728 9.728-9.728 26.112 0 35.84 9.728 9.728 26.112 9.728 35.84 0l143.872-143.36c10.24-9.728 10.24-26.112 0-35.84z" p-id="6102"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7225_67990)">
|
||||
<path d="M18 0H0V18H18V0Z" fill="white" fill-opacity="0.01"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 5.625H15L13.875 16.5H4.125L3 5.625Z" stroke="#5B6871" stroke-width="1.125" stroke-linejoin="round"/>
|
||||
<path d="M7.50073 9.37598V13.1261" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round"/>
|
||||
<path d="M10.5009 9.375V13.1241" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round"/>
|
||||
<path d="M4.5 5.62496L10.6216 1.125L13.5 5.625" stroke="#5B6871" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7225_67990">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 795 B |
@@ -0,0 +1 @@
|
||||
<svg t="1784812882139" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3484" width="24" height="24"><path d="M446.112323 177.545051c137.567677 0.219798 252.612525 104.59798 266.162424 241.493333 13.562828 136.895354-78.778182 261.818182-213.617777 289.008485-134.852525 27.203232-268.386263-52.156768-308.945455-183.608889s25.018182-272.252121 151.738182-325.779394A267.235556 267.235556 0 0 1 446.112323 177.545051m0-62.060607c-182.794343 0-330.989899 148.195556-330.989899 330.989899s148.195556 330.989899 330.989899 330.989899 330.989899-148.195556 330.989899-330.989899-148.195556-330.989899-330.989899-330.989899z m431.321212 793.341415a30.849293 30.849293 0 0 1-21.94101-9.102223l-157.220202-157.220202c-11.752727-12.179394-11.584646-31.534545 0.37495-43.50707 11.972525-11.972525 31.327677-12.140606 43.494141-0.37495l157.220202 157.220202a31.036768 31.036768 0 0 1 6.723232 33.810101 31.004444 31.004444 0 0 1-28.651313 19.174142z m0 0" p-id="3485"></path></svg>
|
||||
|
After Width: | Height: | Size: 1014 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_161_3866" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3333 0H10.6667V10.6667H0V13.3333H10.6667V24H13.3333V13.3333H24V10.6667H13.3333V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_161_3866)">
|
||||
<rect x="-4" y="-4" width="32" height="32" fill="#646566"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 476 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_161_2681" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="3" width="12" height="12">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7203 3.21967L14.7809 4.28033L10.061 9.00025L14.7809 13.7202L13.7203 14.7809L9.00034 10.0609L4.28039 14.7809L3.21973 13.7202L7.93968 9.00025L3.21975 4.28033L4.28041 3.21967L9.00034 7.93959L13.7203 3.21967Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_161_2681)">
|
||||
<rect width="18" height="18" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 560 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path fill="#4aaff0" d="M512,585 C415,455 330,398 215,395 C185,395 160,415 160,450 L160,770 C160,815 190,838 240,845 C340,856 430,872 512,888 C594,872 684,856 784,845 C834,838 864,815 864,770 L864,450 C864,415 839,395 809,395 C694,398 609,455 512,585 Z"/>
|
||||
<path fill="#4aaff0" d="M218,312 C335,292 462,352 514,462 C452,404 334,354 218,378 Z"/>
|
||||
<circle fill="#4aaff0" cx="218" cy="345" r="33"/>
|
||||
<path fill="#80d976" d="M508,462 C455,310 525,145 700,105 C758,250 668,425 508,462 Z"/>
|
||||
<path fill="none" stroke="#4aaff0" stroke-width="44" stroke-linecap="round" d="M210,935 Q285.5,881 361,935 T512,935 T663,935 T814,935"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 701 B |
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 表单中心本地 Mock(vite-plugin-mock,仅 `npm run dev:mock` 即 --mode mock 生效)。
|
||||
*
|
||||
* 背景与授权口径:执行手册 §5 P1 的降级取证先例——本地 CAS/UserBase 不可用时,
|
||||
* 以本文件的模块树验证「壳渲染 + 左导航 + path 空节点跳过」,并把结论记入问题单。
|
||||
*
|
||||
* 形状严格对齐后端 FormBaseController.toFrontendTree 的下发字段
|
||||
* (id/modId/name/parentId/page/level/icon/orderNumber/usable/children,信封 {code,msg,data}),
|
||||
* 模块行取执行手册 §2.2 的建议表(中心管理员四模块行 + 我的填写 + 数据全量权限锚点行)。
|
||||
*/
|
||||
import type { IModule } from "@/types/Basics";
|
||||
|
||||
const ok = (data: any) => ({ code: 0, msg: "", data });
|
||||
|
||||
/** 中心应用(form-center)自身的模块树 */
|
||||
const OWN_MODULES: IModule[] = [
|
||||
{
|
||||
id: 9101,
|
||||
modId: 9101,
|
||||
name: "表单设计",
|
||||
parentId: 0,
|
||||
page: "/admin/forms",
|
||||
level: 1,
|
||||
icon: "word",
|
||||
orderNumber: 1,
|
||||
usable: true,
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 9102,
|
||||
modId: 9102,
|
||||
name: "数据管理",
|
||||
parentId: 0,
|
||||
page: "/admin/data",
|
||||
level: 1,
|
||||
icon: "category",
|
||||
orderNumber: 2,
|
||||
usable: true,
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 9103,
|
||||
modId: 9103,
|
||||
name: "统计",
|
||||
parentId: 0,
|
||||
page: "/admin/stats",
|
||||
level: 1,
|
||||
icon: "tongji",
|
||||
orderNumber: 3,
|
||||
usable: true,
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 9104,
|
||||
modId: 9104,
|
||||
name: "字典管理",
|
||||
parentId: 0,
|
||||
page: "/admin/dicts",
|
||||
level: 1,
|
||||
icon: "zidingyi",
|
||||
orderNumber: 4,
|
||||
usable: true,
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 9105,
|
||||
modId: 9105,
|
||||
name: "我的填写",
|
||||
parentId: 0,
|
||||
page: "/my",
|
||||
level: 1,
|
||||
// 平台未配置图标 → layout 兜底 icon-list(规范 §4 / Notice 13afb22)
|
||||
icon: "",
|
||||
orderNumber: 5,
|
||||
usable: true,
|
||||
children: []
|
||||
},
|
||||
{
|
||||
// path 空行 = 纯权限锚点(customId=fc:scope:all):后端按授权集合扫描消费,
|
||||
// 前端导航渲染必须跳过(手册 §2.2)。modId 由后端 resolveModId 回退主键 id(非数字 customId)
|
||||
id: 9106,
|
||||
modId: 9106,
|
||||
name: "数据全量",
|
||||
parentId: 0,
|
||||
page: "",
|
||||
level: 1,
|
||||
icon: "",
|
||||
orderNumber: 6,
|
||||
usable: true,
|
||||
children: []
|
||||
}
|
||||
];
|
||||
|
||||
export default [
|
||||
{
|
||||
url: "/api/base/GetUserInfo",
|
||||
method: "get",
|
||||
response: () =>
|
||||
ok({
|
||||
id: 186532,
|
||||
account: "mockadmin",
|
||||
name: "Mock 管理员",
|
||||
profilePicture: "",
|
||||
school: { id: 3260, name: "云海附属中学" }
|
||||
})
|
||||
},
|
||||
{
|
||||
url: "/api/base/GetUserMod",
|
||||
method: "get",
|
||||
// 与后端同一条判定:appCode 指向别的应用 → 空树(H4 未上线口径,P3:前端不得回落本应用树)
|
||||
response: ({ query }: any) => {
|
||||
const target = String(query?.appCode ?? "").trim();
|
||||
if (target && target.toLowerCase() !== "form-center") return ok([]);
|
||||
return ok(OWN_MODULES);
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,17 @@
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
html,body,#app{
|
||||
height: 100%;
|
||||
}
|
||||
body{
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
min-width: 1200px;
|
||||
}
|
||||
a{
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 License: none (public domain)*/
|
||||
div, span, applet, object, iframe,h1, h2, h3, h4, h5, h6, p, blockquote, pre,a, abbr, acronym, address, big, cite, code,del, dfn, em, img, ins, kbd, q, s, samp,small, strike, strong, sub, sup, tt, var,b, u, i, center,dl, dt, dd, ol, ul, li,fieldset, form, label, legend,table, caption, tbody, tfoot, thead, tr, th, td,article, aside, canvas, details, embed,figure, figcaption, footer, header, hgroup,menu, nav, output, ruby, section, summary,time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; vertical-align: baseline;}/* HTML5 display-role reset for older browsers */article, aside, details, figcaption, figure,footer, header, hgroup, menu, nav, section { display: block;}ol, ul { list-style: none;}blockquote, q { quotes: none;}blockquote:before, blockquote:after,q:before, q:after { content: ''; content: none;}table { border-collapse: collapse; border-spacing: 0;}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*滚动条样式 */
|
||||
|
||||
/*第一种样式,所有的滚动条,都默认这个样子*/
|
||||
|
||||
*::-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);
|
||||
}
|
||||
|
||||
/*第二种样式结束*/
|
||||
@@ -0,0 +1,35 @@
|
||||
.my-table {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #c7c7c7;
|
||||
border-right: 1px solid #c7c7c7;
|
||||
border-collapse: separate;
|
||||
|
||||
.td-title {
|
||||
background: #fafafa;
|
||||
vertical-align: middle;
|
||||
color: #111;
|
||||
font-weight: 550;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #fafafa;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
td {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border-top: 1px solid #c7c7c7;
|
||||
border-left: 1px solid #c7c7c7;
|
||||
margin-left: -1px;
|
||||
margin-top: -1px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
table.small-table {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 5px;
|
||||
th {
|
||||
background-color: #fafafa;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
td {
|
||||
padding: 5px 20px;
|
||||
}
|
||||
tr:first-child td {
|
||||
padding-top: 10px;
|
||||
}
|
||||
tr:last-child td {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.input-box {
|
||||
width: 100px;
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
}
|
||||
.ivu-input-wrapper {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
$liColor: #969799, #e6a23c, #45b972, #eb0404;
|
||||
|
||||
@each $c in $liColor {
|
||||
$i: index($liColor, $c);
|
||||
.status-color-#{$i} {
|
||||
color: $c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
@import "base/base";
|
||||
@import "base/bscroll.css";
|
||||
@import "base/reset";
|
||||
|
||||
@import "utils/mixins";
|
||||
@import "utils/utils";
|
||||
|
||||
@import "componets/status";
|
||||
@import "componets/small_table";
|
||||
@import "componets/my_table";
|
||||
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 品牌色一律走通道主题令牌(规范 §1 禁止写死品牌色),兜底值仅在通道未到达时生效 */
|
||||
.base-subtitle {
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
line-height: 24px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 3px;
|
||||
background: var(--brand, #577afb);
|
||||
height: 15px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h4.second-title {
|
||||
@extend .base-subtitle;
|
||||
&::before {
|
||||
background: var(--brand, #577afb);
|
||||
}
|
||||
}
|
||||
|
||||
h4.subtitle {
|
||||
@extend .second-title;
|
||||
&::before {
|
||||
background: #bda366;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 表格加载(规范 §4 表格加载行) =================
|
||||
图形=通道下发的 .ysch-spin(11 点环 · 2s · steps(11,end) · mask+currentColor 随五档),
|
||||
本仓零图形资产:禁 loading.gif、禁本地 data-URI 副本、禁写死色值。
|
||||
.table-spin-load 只管布局(60×90 + 文案),文案与图形同色 var(--brand)。 */
|
||||
.table-spin-load {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 90px;
|
||||
padding-top: 60px;
|
||||
border: none;
|
||||
color: var(--brand);
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
.table-spin-load .ysch-spin {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-left: -30px;
|
||||
font-size: 60px; /* .ysch-spin 尺寸=1em,60px 图形用字号给 */
|
||||
}
|
||||
|
||||
.ivu-table-cell-tree {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* 修复表格幻影省略号:.ivu-table-cell 自带 overflow:hidden + text-overflow:ellipsis,
|
||||
tooltip 列的包装层(.ivu-tooltip 行内块 + width:100%)未溢出也会被画出「…」。
|
||||
注意不要再给操作列的裸 .ivu-tooltip 加 display:block——会以更高特异性顶掉 App.vue 的横排规则,
|
||||
使图标按钮竖排(Notice 2026-10-10 踩过,已回退) */
|
||||
.ivu-table .ivu-table-cell > .ivu-table-cell-tooltip {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* ================= 表格操作列(utils/tableActions.ts)=================
|
||||
.op-cell:flex 横排容器——把 iView Tooltip/Dropdown 的块级包装层降级为 flex item,强制横排
|
||||
(根因:.operating-btn 在通道基线 display:block,.ivu-tooltip 包装层默认 block,逐钮竖排)。
|
||||
先落工程内,四端同构后此段上提 YschTheme 主题层(规范 §5.4)。 */
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 固定列层不吞下层事件 =================
|
||||
iView 分组表 fixed:right 列的固定层是全表覆盖的独立副本,命中测试落到它上面会让下层控件
|
||||
「看得见、点不动」。固定层只做视觉覆盖:容器 pointer-events:none、真实单元格 auto。 */
|
||||
.ivu-table-fixed,
|
||||
.ivu-table-fixed-right {
|
||||
pointer-events: none;
|
||||
|
||||
th,
|
||||
td,
|
||||
.ivu-table-cell {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 省略号单元格内的 Tooltip =================
|
||||
iView 的 Tooltip 根节点与 .ivu-tooltip-rel 都是 inline-block,放进 column.ellipsis 单元格
|
||||
会把 text-overflow:ellipsis 变成硬裁切(省略号不渲染)。仅在省略号单元格内降级为 inline。 */
|
||||
.ivu-table-cell-ellipsis {
|
||||
.ivu-tooltip,
|
||||
.ivu-tooltip-rel {
|
||||
display: inline;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@mixin tag_mixin($bgColor,$textColor) {
|
||||
width:64px;
|
||||
height:20px ;
|
||||
padding: 0 5px;
|
||||
background: $bgColor;
|
||||
color: $textColor;
|
||||
font-size: 12px;
|
||||
border-radius: 2px 2px 2px 2px;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
// utils 工具// 颜色
|
||||
|
||||
@import "./varible";
|
||||
.bg-error {
|
||||
background-color: $color-error !important;
|
||||
}
|
||||
.text-error {
|
||||
color: $color-error !important;
|
||||
}
|
||||
.text-success {
|
||||
color: $color-success !important;
|
||||
}
|
||||
.text-warning {
|
||||
color: $color-warning !important;
|
||||
}
|
||||
.text-primary {
|
||||
color: var(--brand, #577afb) !important;
|
||||
}
|
||||
.text-normal {
|
||||
color: #404042 !important;
|
||||
}
|
||||
.text-black {
|
||||
color: #000;
|
||||
}
|
||||
.text-dark {
|
||||
color: #333;
|
||||
}
|
||||
.text-deep {
|
||||
color: #555;
|
||||
}
|
||||
.text-weak {
|
||||
color: #b3b3b3;
|
||||
}
|
||||
.text-white {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.text-error.disabled {
|
||||
color: #aaa !important;
|
||||
}
|
||||
|
||||
// 文字对齐
|
||||
.tl {
|
||||
text-align: left;
|
||||
}
|
||||
.tc {
|
||||
text-align: center;
|
||||
}
|
||||
.tr {
|
||||
text-align: right;
|
||||
}
|
||||
// 浮动与清除浮动
|
||||
.fl {
|
||||
float: left;
|
||||
}
|
||||
.fr {
|
||||
float: right;
|
||||
}
|
||||
.fix {
|
||||
*zoom: 1;
|
||||
}
|
||||
.fix:after {
|
||||
display: table;
|
||||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
// 显示
|
||||
.dn {
|
||||
display: none;
|
||||
}
|
||||
.di {
|
||||
display: inline;
|
||||
}
|
||||
.db {
|
||||
display: block;
|
||||
}
|
||||
.dib {
|
||||
display: inline-block;
|
||||
}
|
||||
.dt {
|
||||
display: table;
|
||||
}
|
||||
div.dib {
|
||||
*display: inline;
|
||||
*zoom: 1;
|
||||
}
|
||||
.vm {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.vib {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ovh {
|
||||
overflow: hidden;
|
||||
}
|
||||
// 定位
|
||||
.pr {
|
||||
position: relative;
|
||||
}
|
||||
.pa {
|
||||
position: absolute;
|
||||
}
|
||||
.pf {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.align-middle {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.align-middle-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@for $i from 1 through 100 {
|
||||
.width-#{$i} {
|
||||
width: $i * 1%;
|
||||
}
|
||||
.height-#{$i} {
|
||||
height: $i * 1px;
|
||||
}
|
||||
// @if $i<50 and $i%2==0 {
|
||||
// @if $i<50 {
|
||||
.ml-#{$i} {
|
||||
margin-left: $i * 1px !important;
|
||||
}
|
||||
.mr-#{$i} {
|
||||
margin-right: $i * 1px !important;
|
||||
}
|
||||
.mt-#{$i} {
|
||||
margin-top: $i * 1px !important;
|
||||
}
|
||||
.mb-#{$i} {
|
||||
margin-bottom: $i * 1px !important;
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
// 字体
|
||||
@for $i from 10 through 28 {
|
||||
.f#{$i} {
|
||||
font-size: $i * 1px;
|
||||
}
|
||||
}
|
||||
|
||||
// .chart-box {
|
||||
// height: 50px;
|
||||
// width: 100%;
|
||||
// border-bottom: 50px solid;
|
||||
// border-image: linear-gradient(90deg, #f55c65 0%, #fcced0 48%, #d4dffc 52%, #7194f5 100%) 50 50 50 50;
|
||||
// }
|
||||
// @for $i from 0 through 50 {
|
||||
// .blue-#{$i} {
|
||||
// background-color: rgba(#7194f5, 0.1 * $i * 0.2);
|
||||
// }
|
||||
// }
|
||||
// @for $i from 0 through 50 {
|
||||
// .red-#{$i} {
|
||||
// background-color: rgba(#f55c65, 0.1 * $i * 0.2);
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,22 @@
|
||||
/*scss变量*/
|
||||
|
||||
// 主题色(品牌主色不在此定义:由平台主题令牌 var(--brand) 经中央静态通道下发,
|
||||
// 本文件只留业务语义色——成功/警告/错误不随主题档位变化)
|
||||
$color-error: #e8684a;
|
||||
$color-success: #45b972;
|
||||
$color-warning: #e9b954;
|
||||
|
||||
// 文字色
|
||||
$color-black: #404042;
|
||||
$color-dark: #333;
|
||||
$color-deep: #555;
|
||||
$color-pl: #999999;
|
||||
$color-weak: #b3b3b3;
|
||||
$color-white: #fff;
|
||||
|
||||
// 背景色
|
||||
$bg-bar: #f9f9f9;
|
||||
$bg-page: #f3f3f3;
|
||||
$bg-page-light: #f9f9f9;
|
||||
|
||||
$border-color: #e9e9e9;
|
||||
@@ -0,0 +1,88 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const Button: typeof import('view-ui-plus')['Button']
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const Form: typeof import('view-ui-plus')['Form']
|
||||
const FormItem: typeof import('view-ui-plus')['FormItem']
|
||||
const Input: typeof import('view-ui-plus')['Input']
|
||||
const Message: typeof import('view-ui-plus')['Message']
|
||||
const Modal: typeof import('view-ui-plus')['Modal']
|
||||
const Select: typeof import('view-ui-plus')['Select']
|
||||
const Table: typeof import('view-ui-plus')['Table']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useForm: typeof import('alova/client')['useForm']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const usePagination: typeof import('alova/client')['usePagination']
|
||||
const useRequest: typeof import('alova/client')['useRequest']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const useWatcher: typeof import('alova/client')['useWatcher']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 页面骨架占位件(T5):只验证「壳 + page-wrapper 骨架」到位,不含领域逻辑。
|
||||
*
|
||||
* 骨架类名对齐中央静态通道(规范 §2.2 回收步 / §4.4 卡片外壳):
|
||||
* .page-wrapper.yh-fill(height:100% + flex 纵向)> .main-container(白卡,margin/padding 各 16)
|
||||
* 列表页真身应在此之下再挂 .table-region(flex:1 1 auto + min-height:0 + overflow:hidden,
|
||||
* 零 padding/border,规范 §4.3)并用 useElementHeight 实测喂 <Table :height>——
|
||||
* 占位页没有表格,故只铺骨架不做实测,避免留下「有 .table-region 却无量高」的假范式。
|
||||
*/
|
||||
defineProps<{
|
||||
/** 页面名(与 UserBase 模块行名称、routes.ts 的 meta.title 同一口径) */
|
||||
title: string;
|
||||
/** 本体交付批次(T6/T7/T8) */
|
||||
stage: string;
|
||||
/** 本批次要在此页落地什么,写给接手的人看 */
|
||||
note: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-wrapper yh-fill">
|
||||
<div class="main-container">
|
||||
<h4 class="subtitle">{{ title }}</h4>
|
||||
<Alert show-icon type="info">
|
||||
本页为 T5 交付的路由骨架占位,页面本体在 {{ stage }} 落地;当前壳(顶栏/左导航/面包屑)已生效。
|
||||
</Alert>
|
||||
<p class="stub-note">{{ note }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.stub-note {
|
||||
margin: 16px 0 0;
|
||||
color: var(--brand, #577afb);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, withDefaults } from "vue";
|
||||
|
||||
interface Props {
|
||||
prefix?: string;
|
||||
name?: string;
|
||||
color?: string;
|
||||
hoverColor?: string;
|
||||
size?: string | number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
prefix: "icon",
|
||||
name: "",
|
||||
color: "#000",
|
||||
});
|
||||
|
||||
const isHover = ref(false);
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
|
||||
const svgStyle = computed(() => {
|
||||
if (!!props.size) return { width: props.size + "px", height: props.size + "px" };
|
||||
else return null;
|
||||
});
|
||||
|
||||
const compColor = computed(() => {
|
||||
if (!props.hoverColor) return props.color;
|
||||
else {
|
||||
return !!isHover.value ? props.hoverColor : props.color;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
@mouseover="isHover = true"
|
||||
@mouseout="isHover = false"
|
||||
aria-hidden="true"
|
||||
:color="compColor"
|
||||
:style="svgStyle"
|
||||
>
|
||||
<use :xlink:href="symbolId" :fill="compColor" :color="compColor" />
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,122 @@
|
||||
<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>
|
||||
<div class="table-spin-load"><i class="ysch-spin"></i>加载中</div>
|
||||
</template>
|
||||
<template v-for="slotName in tableSlotNames" :key="slotName" v-slot:[slotName]="slotProps">
|
||||
<slot :name="slotName" v-bind="slotProps" />
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<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, useSlots, watch } from 'vue'
|
||||
import { useElementHeight } from '@/utils/hook/useElementHeight'
|
||||
import { OperatorEnum } from '@/types'
|
||||
|
||||
interface Props {
|
||||
dataApi: any
|
||||
columns: 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
|
||||
}>()
|
||||
const slots = useSlots()
|
||||
|
||||
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[]>([])
|
||||
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({ pageIndex: current, pageSize: size }),
|
||||
{
|
||||
total: res => props.formatResponse(res).total,
|
||||
data: res => props.formatResponse(res).list,
|
||||
initialPage: 1,
|
||||
// 页长记忆(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
|
||||
localStorage.setItem('customPageCount', String(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 (type === OperatorEnum.sel) p = 1
|
||||
if (p < 1) p = 1
|
||||
|
||||
page.value = p
|
||||
sendLoad()
|
||||
}
|
||||
|
||||
defineExpose({ list, refreshData, reloadData })
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,266 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { resolveMenuPage } from "@/utils/menuRoute";
|
||||
import { flattenNav, type NavNode } from "@/utils/menuNav";
|
||||
import UserAvatar from "@/components/UserAvatar.vue";
|
||||
import defaultLogo from "@/assets/image/yunhai-logo.svg";
|
||||
|
||||
const props = defineProps<{ layoutData: Record<string, any> }>();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const currPath = ref<string>("");
|
||||
const navPath = ref<Array<{ name: string; page?: string }>>([]);
|
||||
/** 分组展开态(按分组行 id 记忆,初始全折叠——与 Notice 现行行为一致) */
|
||||
const openGroups = ref<Record<number, boolean>>({});
|
||||
|
||||
const baseHost = computed(() => props.layoutData?.baseHost || "");
|
||||
const ssoHost = computed(() => props.layoutData?.ssoHost || "");
|
||||
const appName = computed(() => props.layoutData?.appName || "表单中心");
|
||||
const userInfo = computed(() => props.layoutData?.userInfo || { name: "用户", avatar: "" });
|
||||
const isAlone = computed(() => props.layoutData?.isAlone ?? false);
|
||||
const navList = computed<NavNode[]>(() => props.layoutData?.navList || []);
|
||||
|
||||
const pathNavStyle = computed(() => (isAlone.value ? { left: "0", width: "100%" } : null));
|
||||
|
||||
/** 扁平化导航项(高亮/面包屑对照表),由 utils/menuNav 的同一棵树导出,跳过规则只有一处实现 */
|
||||
const flatNav = computed(() => flattenNav(navList.value));
|
||||
|
||||
/**
|
||||
* 顶栏 Logo:localStorage["yh-logo-url"](平台键,yh-theme-init 异步补齐段写入)优先,
|
||||
* 本仓资产兜底——绝无空 src/裂图。刷新:mounted 读一次 + 监听 yh-appearance-updated(规范 §3)。
|
||||
*/
|
||||
const logoUrl = ref<string>(defaultLogo);
|
||||
const readLogoUrl = () => {
|
||||
let v: string | null = null;
|
||||
try {
|
||||
v = localStorage.getItem("yh-logo-url");
|
||||
} catch {
|
||||
v = null;
|
||||
}
|
||||
logoUrl.value = v || defaultLogo;
|
||||
};
|
||||
const onLogoError = () => {
|
||||
if (logoUrl.value !== defaultLogo) logoUrl.value = defaultLogo;
|
||||
};
|
||||
|
||||
onBeforeMount(() => routeChange(route));
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => routeChange(route)
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
readLogoUrl();
|
||||
document.addEventListener("yh-appearance-updated", readLogoUrl);
|
||||
});
|
||||
onBeforeUnmount(() => document.removeEventListener("yh-appearance-updated", readLogoUrl));
|
||||
|
||||
/** 面包屑/高亮:优先按 path 匹配导航项,匹配不到再退回 modId 对照 */
|
||||
const routeChange = (to: any) => {
|
||||
const meta: any = to.meta || {};
|
||||
const { modId, title, parentId } = meta;
|
||||
const navArr: Array<{ name: string; page?: string }> = [];
|
||||
const items = flatNav.value;
|
||||
|
||||
const find: any =
|
||||
items.find((x: any) => x.page && x.page === to.path) || items.find((x: any) => x.modId === modId);
|
||||
if (find) {
|
||||
currPath.value = find.page || "";
|
||||
if (find.pid) {
|
||||
const parent = navList.value.find((x) => x.id === find.pid);
|
||||
if (parent) navArr.push({ name: parent.name });
|
||||
}
|
||||
navArr.push({ name: find.name || title, page: find.page });
|
||||
navPath.value = navArr;
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentId) {
|
||||
const parent = items.find((x: any) => x.modId === parentId);
|
||||
if (parent) {
|
||||
currPath.value = parent.page || "";
|
||||
if (parent.pid) {
|
||||
const gp = navList.value.find((x) => x.id === parent.pid);
|
||||
if (gp) navArr.push({ name: gp.name });
|
||||
}
|
||||
navArr.push({ name: parent.name, page: parent.page });
|
||||
navArr.push({ name: title });
|
||||
navPath.value = navArr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const r = router.getRoutes().find((rt: any) => (rt.meta as any)?.modId === modId);
|
||||
if (r) {
|
||||
currPath.value = r.path;
|
||||
navPath.value = [{ name: (r.meta as any).title, page: r.path }];
|
||||
return;
|
||||
}
|
||||
|
||||
currPath.value = "";
|
||||
navPath.value = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* 菜单图标:读用户中心模块管理配置的 YschIcons 图标名(GetUserMod 的 icon 字段,如 list → icon-list)。
|
||||
* icons.css(ysch-icons 掩码版,基类 iconfont,类名 icon-<名称>)由中央静态通道下发;
|
||||
* 平台未配置图标时统一兜底 icon-list(规范 §4 / Notice 13afb22:菜单项恒有图标,保整列缩进/行高一致)
|
||||
*/
|
||||
const getMenuIcon = (item?: any): string => `icon-${String(item?.icon || "").trim() || "list"}`;
|
||||
|
||||
/** 统一解析菜单项目标:模板高亮与点击都走这里,避免 raw page 与解析后 path 不一致 */
|
||||
const resolveItemPage = (item: NavNode) => resolveMenuPage(item.raw, item.modId, item.name);
|
||||
const pageOf = (item: NavNode): string => resolveItemPage(item).path;
|
||||
|
||||
const linkMenu = (item: NavNode) => {
|
||||
const { path, external } = resolveItemPage(item);
|
||||
if (!path) {
|
||||
// 解析不出有效页面:显式提示,而不是静默不动或被兜底路由吞到入口页
|
||||
Message.warning(`菜单「${item?.name || "未命名"}」未配置有效页面路径,请检查基础平台模块设置`);
|
||||
return;
|
||||
}
|
||||
if (external) {
|
||||
window.location.href = path;
|
||||
return;
|
||||
}
|
||||
if (path === route.path) router.go(0);
|
||||
else router.push(path);
|
||||
};
|
||||
|
||||
const childChange = (item: NavNode) => {
|
||||
openGroups.value = { ...openGroups.value, [item.id]: !openGroups.value[item.id] };
|
||||
};
|
||||
|
||||
const linkUserInfo = () => {
|
||||
// 头像/姓名统一跳 UserHome 设置页「个人信息」pane(Notice 口径,旧 /v/userinfo 已下线)
|
||||
window.location.href = baseHost.value + "/userhome/front/index.html#/settings?pane=profile";
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
const url = "/logout?service=" + window.location.href;
|
||||
window.location.href = ssoHost.value + url;
|
||||
};
|
||||
|
||||
function linkHomePage() {
|
||||
if (baseHost.value) {
|
||||
// 旧 BschHome 已下线(线上 404),首页统一指向用户中心 userhome
|
||||
window.location.href = baseHost.value + "/userhome/front/index.html";
|
||||
} else {
|
||||
window.location.href = "/";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-wrap">
|
||||
<header class="header-container fix">
|
||||
<!-- 云海 logo 徽标(结构与通道 base.layout.css .logo-badge 对齐;子应用不展示 LOGO 文字,规范 §3 红线) -->
|
||||
<span class="logo-badge fl"><img :src="logoUrl" alt="云海" @error="onLogoError" /></span>
|
||||
<div class="delimiter fl align-middle"></div>
|
||||
<h4 class="fl align-middle">{{ appName }}</h4>
|
||||
<div class="school-bg"></div>
|
||||
<div class="header-aside fr">
|
||||
<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">
|
||||
{{ userInfo.name }}
|
||||
</a>
|
||||
<div class="delimiter fl align-middle"></div>
|
||||
<a href="javascript:;" @click="logout" class="logout fl align-middle" title="退出登陆">
|
||||
<i class="iconfont icon-logout"></i>
|
||||
</a>
|
||||
</div>
|
||||
<!-- 配色切换器不建(规范 §3 红线):主题设置入口已收敛 UserHome,本地切换会被服务端生效值打回 -->
|
||||
</header>
|
||||
|
||||
<div class="left-menu" v-if="!isAlone">
|
||||
<div class="menu-top-box"></div>
|
||||
<ul>
|
||||
<template v-for="item in navList" :key="'menu_li_' + item.id">
|
||||
<template v-if="item.group">
|
||||
<li>
|
||||
<a href="javascript:;" @click="childChange(item)">
|
||||
<i :class="openGroups[item.id] ? 'arrow-down' : 'arrow-up'"></i>
|
||||
<i :class="['iconfont', getMenuIcon(item)]"></i>
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</li>
|
||||
<transition name="fade_child_ul">
|
||||
<div class="child-item-wrap" v-show="openGroups[item.id]">
|
||||
<li
|
||||
:class="pageOf(citem) == currPath ? 'active child-item' : 'child-item'"
|
||||
v-for="citem in item.children"
|
||||
:key="citem.id"
|
||||
>
|
||||
<a href="javascript:;" @click="linkMenu(citem)">
|
||||
{{ citem.name }}
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
<li v-else :class="pageOf(item) == currPath ? 'active' : ''">
|
||||
<a href="javascript:;" @click="linkMenu(item)">
|
||||
<i :class="['iconfont', getMenuIcon(item)]"></i>
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<div class="clear-box"></div>
|
||||
</div>
|
||||
|
||||
<div class="path-nav" :style="pathNavStyle">
|
||||
<Breadcrumb separator=">">
|
||||
<a href="javascript:;" @click="linkHomePage">
|
||||
<BreadcrumbItem>
|
||||
<span class="crumb-home">
|
||||
<i class="iconfont icon-shouye"></i>
|
||||
首页
|
||||
</span>
|
||||
</BreadcrumbItem>
|
||||
</a>
|
||||
<BreadcrumbItem v-if="!isAlone" to="/">{{ appName }}</BreadcrumbItem>
|
||||
<template v-for="(item, index) in navPath" :key="index">
|
||||
<BreadcrumbItem v-if="index != navPath.length - 1" :to="item.page">
|
||||
{{ item.name }}
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem v-else>{{ item.name }}</BreadcrumbItem>
|
||||
</template>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 仅覆盖/补充必要的动态样式,其余由中央静态通道的 base.layout.css(骨架 + 皮肤)提供。
|
||||
本地零 layout.css/icons.css(规范 §2.2 回收步,V22) */
|
||||
/* 菜单图标(通道下发的 ysch-icons 掩码类)与文字间距 */
|
||||
.left-menu a .iconfont {
|
||||
margin-right: 8px;
|
||||
}
|
||||
/* 顶栏已由通道皮肤改为 flex 垂直居中,这里不再用 float 时代的 13px 顶偏移补偿,
|
||||
仅留右侧间距;徽标大小/圆底/阴影属皮肤 */
|
||||
.header-container .logo-badge {
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
/* 面包屑「首页」与后续级目的文字对齐(Notice 13afb22 refined 版,规范 §4):
|
||||
此前用 inline-flex,行内级盒的基线取首项——16px 无字图标(基线=方块底缘)把整盒顶高约 4px,
|
||||
首页文字比相邻级目高。改回普通行内流:图标以 -0.125em 基线偏移做光学居中,
|
||||
与相邻级目共用同一行盒基线,天然对齐 */
|
||||
.crumb-home .iconfont {
|
||||
position: static;
|
||||
top: 0;
|
||||
margin-right: 4px;
|
||||
vertical-align: -0.125em;
|
||||
}
|
||||
</style>
|
||||
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,4 +1,39 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
createApp(App).mount("#app");
|
||||
import App from "./App.vue";
|
||||
import router from "@/router";
|
||||
|
||||
import utils from "@/utils";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
// @ts-ignore
|
||||
import ViewUIPlus from "view-ui-plus";
|
||||
import mittBus from "@/utils/mitt";
|
||||
|
||||
import "view-ui-plus/dist/styles/viewuiplus.css";
|
||||
|
||||
import "@/assets/styles/main.scss";
|
||||
// 布局/主题/图标(base.layout.css + iview.override.css + icons.css)由中央静态通道下发:
|
||||
// index.html 的 yh-theme-init.js 在 DOMContentLoaded 后按 manifest 版本追加 <link>,
|
||||
// 天然排在本文件组件库样式之后 —— 本地零 layout.css/icons.css(规范 §2.2 回收步 / V22)
|
||||
// 导入自定义指令
|
||||
import directive from "@/utils/directive/index";
|
||||
|
||||
import "virtual:uno.css";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
app.use(ViewUIPlus);
|
||||
|
||||
import config from "@/assets/config/host.config";
|
||||
app.config.globalProperties.$conf = config;
|
||||
app.config.globalProperties.$utils = utils;
|
||||
app.config.globalProperties.$bus = mittBus;
|
||||
|
||||
directive(app);
|
||||
|
||||
import "virtual:svg-icons-register";
|
||||
|
||||
app.mount("#app");
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import routes from "./routes";
|
||||
|
||||
/**
|
||||
* hash 路由(Notice / flow-center 同族口径):产物部署在 /{ctx}/front/index.html,
|
||||
* 静态资源由 projman 的 static-routes.conf 下发到 /form-center/,路由段全部在 # 之后——
|
||||
* 所以 UserBase 模块行配的 path 就是这里的路径(/admin/forms 等),完整链接形如
|
||||
* /form-center/front/index.html#/admin/forms(手册 §1 的 /form-center/f/{code} 即 ctx + #/f/{code})。
|
||||
*/
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
|
||||
/**
|
||||
* 表单中心路由骨架(T5)——一个 SPA 承载全部形态(执行手册 §2.1):
|
||||
* 管理端 /admin/*、填报页 /f/:code、快应用上下文 /app/:appCode/*、我的填写 /my。
|
||||
* 同仓同构建同一条静态路由,不建公开站、不拆第二个 front。
|
||||
*
|
||||
* 两处对齐约束:
|
||||
* ① path 必须与 UserBase app_module.path 逐字一致(手册 §2.2 表;layout 优先按 path
|
||||
* 匹配菜单高亮与面包屑);
|
||||
* ② <b>本表刻意不写 meta.modId</b>——规范 §4「modId 全量对齐后端 /base/GetUserMod,禁臆造」:
|
||||
* 本应用模块行的 customId 除权限锚点 fc:scope:all 外均未配(手册 §2.2 表为「—」),
|
||||
* 后端 resolveModId 因此回退 app_module 主键 id,该值跨环境不稳定,写进路由必然失配。
|
||||
* 详情页需要父级高亮时用 meta.parentId(同 Notice/flow-center 范式)。
|
||||
*
|
||||
* 页面本体分任务落地:管理面/设计器 T6、填报页 T7、数据与统计与快应用 T8;
|
||||
* 本轮 T5 只交付壳与四形态占位页,占位页不含领域逻辑。
|
||||
*/
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
// 应用入口:拿到授权模块树后由 App.vue 跳第一个可用模块;无模块时留在此页给引导
|
||||
path: "/",
|
||||
name: "fc_entry",
|
||||
component: () => import("@/views/Entry.vue"),
|
||||
meta: { title: "表单中心" },
|
||||
},
|
||||
/* ---------------- 管理端(/admin/*,菜单行 path 与之逐字一致) ---------------- */
|
||||
{
|
||||
path: "/admin/forms",
|
||||
name: "fc_admin_forms",
|
||||
component: () => import("@/views/admin/Forms.vue"),
|
||||
meta: { title: "表单设计" },
|
||||
},
|
||||
{
|
||||
path: "/admin/data",
|
||||
name: "fc_admin_data",
|
||||
component: () => import("@/views/admin/Data.vue"),
|
||||
meta: { title: "数据管理" },
|
||||
},
|
||||
{
|
||||
path: "/admin/stats",
|
||||
name: "fc_admin_stats",
|
||||
component: () => import("@/views/admin/Stats.vue"),
|
||||
meta: { title: "统计" },
|
||||
},
|
||||
{
|
||||
path: "/admin/dicts",
|
||||
name: "fc_admin_dicts",
|
||||
component: () => import("@/views/admin/Dicts.vue"),
|
||||
meta: { title: "字典管理" },
|
||||
},
|
||||
/* ---------------- 填报页(分享/门户/快应用直落,code 寻址) ---------------- */
|
||||
{
|
||||
path: "/f/:code",
|
||||
name: "fc_fill",
|
||||
component: () => import("@/views/Fill.vue"),
|
||||
meta: { title: "填报" },
|
||||
},
|
||||
/* ---------------- 快应用上下文(目标应用的模块树 + 范围矩阵,T8 落地) ---------------- */
|
||||
{
|
||||
path: "/app/:appCode",
|
||||
name: "fc_app",
|
||||
component: () => import("@/views/AppContext.vue"),
|
||||
meta: { title: "应用表单" },
|
||||
},
|
||||
{
|
||||
path: "/app/:appCode/:view",
|
||||
name: "fc_app_view",
|
||||
component: () => import("@/views/AppContext.vue"),
|
||||
meta: { title: "应用表单" },
|
||||
},
|
||||
/* ---------------- 我的填写(全员授权行) ---------------- */
|
||||
{
|
||||
path: "/my",
|
||||
name: "fc_my_fill",
|
||||
component: () => import("@/views/My.vue"),
|
||||
meta: { title: "我的填写" },
|
||||
},
|
||||
// 兜底:外部深链/残留配置落到未登记路由时不白屏,回入口由壳决定首个可用页
|
||||
{ path: "/:pathMatch(.*)*", redirect: "/" },
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
/**
|
||||
* 全局 store(notice/front 基线形态)。
|
||||
* 本轮 T5 壳未消费业务态,保留 userRole(school/平台)这一子应用通用位,
|
||||
* 表单/答卷/设计器的态随 T6~T8 按需开切片,不提前建空壳。
|
||||
*/
|
||||
export const useDefaultStore = defineStore("defaultStore", {
|
||||
state: () => ({
|
||||
userRole: "school" as string
|
||||
}),
|
||||
actions: {
|
||||
setUserRole(role: string) {
|
||||
this.userRole = role;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 公共类型(fork notice/front 的 types/Basics.d.ts,剥 notice 领域字段)
|
||||
*/
|
||||
|
||||
export interface IObj {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/** 列表页配置项:useTableHeight 的入参(旧模型,仅 TablePage 系旧组件消费,新页用 .table-region 实测) */
|
||||
export interface UseTableSetting {
|
||||
/** 是否有查询面板 */
|
||||
isSearch?: boolean;
|
||||
/** 是否有分页 */
|
||||
isPage?: boolean;
|
||||
/** 是否是 tabs 布局 */
|
||||
isTabs?: boolean;
|
||||
/** 是否有按钮操作 */
|
||||
isOperat?: boolean;
|
||||
/** 额外的高度 */
|
||||
otherH?: number;
|
||||
}
|
||||
|
||||
/** 本应用运行时配置(host.config.ts 的形态,来源优先级见该文件注释) */
|
||||
export interface BaseConf {
|
||||
baseHost: string;
|
||||
ssoHost: string;
|
||||
serviceHost: string;
|
||||
isMock: boolean;
|
||||
isDebug?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/** 后端 /api/base/GetUserMod 下发的模块树节点(FormBaseController.toFrontendTree 的字段形状) */
|
||||
export interface IModule {
|
||||
id: number;
|
||||
/** 平台 app_module.custom_id(数字)或主键 id;非数字锚点行由后端回退为 id */
|
||||
modId: number | string;
|
||||
name: string;
|
||||
parentId: number;
|
||||
/** 前端路由 path(空串=纯权限锚点,不进导航,见 utils/menuNav.ts) */
|
||||
page: string;
|
||||
level: number;
|
||||
/** 平台配置的 YschIcons 图标名(如 list → icon-list),未配置由前端兜底 */
|
||||
icon: string;
|
||||
orderNumber: number;
|
||||
usable: boolean;
|
||||
children: IModule[];
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 全局共享类型(fork notice/front 的 types/index.ts,只留壳与表格范式用得到的部分;
|
||||
* 表单/答卷值形状随 T6/T7 落地时再补,避免提前定义与契约漂移)
|
||||
*/
|
||||
|
||||
/** 表格列表页刷新语义(TablePageAlovaNew.reloadData 的入参) */
|
||||
export enum OperatorEnum {
|
||||
add,
|
||||
del,
|
||||
edit,
|
||||
sel,
|
||||
}
|
||||
|
||||
export type { IModule } from "./Basics";
|
||||
@@ -0,0 +1,156 @@
|
||||
import { createAlova, Method, StatesHook } from "alova";
|
||||
import fetchAdapter from "alova/fetch";
|
||||
import VueHook from "alova/vue";
|
||||
import Utils from "..";
|
||||
/* ============= 类型定义 ============= */
|
||||
export interface RequestHooks {
|
||||
/** 前置票据/通用头注入 */
|
||||
injectHeaders?: (method: Method) => void;
|
||||
/** 加载开始 */
|
||||
onLoadingStart?: () => void;
|
||||
/** 加载结束 */
|
||||
onLoadingEnd?: () => void;
|
||||
/** 业务错误提示(code !== 0)*/
|
||||
onBizError?: (msg: string) => void;
|
||||
/** http 错误提示(status !== 200)*/
|
||||
onHttpError?: (msg: string) => void;
|
||||
}
|
||||
|
||||
/** 和后端约定的通用响应格式 */
|
||||
export interface BschResponse<T = any> {
|
||||
code: number;
|
||||
msg?: string;
|
||||
data: T;
|
||||
urlToRedirectTo?: string;
|
||||
}
|
||||
|
||||
/* ============= 默认空实现 ============= */
|
||||
const defHooks: Required<RequestHooks> = {
|
||||
injectHeaders: () => {},
|
||||
onLoadingStart: () => {},
|
||||
onLoadingEnd: () => {},
|
||||
onBizError: () => {},
|
||||
onHttpError: () => {},
|
||||
};
|
||||
|
||||
/* ============= 错误处理(原 axios 逻辑平移) ============= */
|
||||
function errorHandle<T>(res: BschResponse<T>, status: number): T {
|
||||
// http 层错误
|
||||
if (status !== 200) {
|
||||
let msg = "网络错误";
|
||||
switch (status) {
|
||||
case 401:
|
||||
case 403:
|
||||
break;
|
||||
case 404:
|
||||
msg = "请求的资源不存在!";
|
||||
break;
|
||||
default:
|
||||
msg = res?.msg || "内部服务错误!";
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
// 业务层错误
|
||||
switch (res.code) {
|
||||
case 0:
|
||||
return res.data ?? (null as T);
|
||||
case 302:
|
||||
console.log(res);
|
||||
// 后端 CasSsoApiRedirectFilter 已在 service 中拼好 ;redirectToReferer=<hex>,
|
||||
// 直接跳转即可;登录后由 CasSsoRefererRedirectFilter 解码 302 回前端页。
|
||||
// 跳转后返回永不 resolve 的挂起 Promise:调用方 await 悬停,
|
||||
// 全局 onBizError 与各页面的 catch 均不再触发(不弹 redirect 报错)
|
||||
window.location.href = res.urlToRedirectTo!;
|
||||
return new Promise(() => {}) as T;
|
||||
case 4:
|
||||
throw new Error(res.msg || "参数错误!");
|
||||
default:
|
||||
throw new Error(res.msg || "业务错误");
|
||||
}
|
||||
}
|
||||
|
||||
/* ============= 工厂函数 ============= */
|
||||
export function createBschAlova(
|
||||
baseURL: string,
|
||||
hooks: RequestHooks = {},
|
||||
statesHook: StatesHook<any> = VueHook,
|
||||
) {
|
||||
const { injectHeaders, onLoadingStart, onLoadingEnd, onBizError, onHttpError } = {
|
||||
...defHooks,
|
||||
...hooks,
|
||||
};
|
||||
|
||||
let reqCount = 0;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
|
||||
/* ---- 请求拦截 ---- */
|
||||
function beforeRequest(method: Method) {
|
||||
reqCount++;
|
||||
if (reqCount === 1) timer = setTimeout(() => onLoadingStart(), 800);
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
if (reqCount === 0) {
|
||||
clearTimeout(timer);
|
||||
onLoadingEnd();
|
||||
}
|
||||
});
|
||||
// 1. 用户注入票据/角色等头
|
||||
injectHeaders(method);
|
||||
|
||||
method.config.credentials = "include";
|
||||
// 2. 通用头
|
||||
method.config.headers = Object.assign(method.config.headers || {}, {
|
||||
yschapirequest: "1",
|
||||
timeout: 20000,
|
||||
yschreferer: Utils.strToHexCharCode(window.location.href),
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- 响应拦截 ---- */
|
||||
async function onSuccess(response: Response) {
|
||||
reqCount--;
|
||||
if (reqCount === 0) {
|
||||
clearTimeout(timer);
|
||||
onLoadingEnd();
|
||||
}
|
||||
const res: BschResponse = await response.json();
|
||||
try {
|
||||
return errorHandle(res, response.status);
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
|
||||
onBizError(e.message);
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onError(err: any) {
|
||||
reqCount = 0;
|
||||
clearTimeout(timer);
|
||||
onLoadingEnd();
|
||||
|
||||
const msg = err?.message || "网络请求出错";
|
||||
onHttpError(msg);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
async function onComplete(method: Method) {
|
||||
if (method.fromCache) {
|
||||
reqCount--;
|
||||
if (reqCount === 0) {
|
||||
clearTimeout(timer);
|
||||
onLoadingEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createAlova({
|
||||
statesHook,
|
||||
requestAdapter: fetchAdapter(),
|
||||
baseURL,
|
||||
cacheFor: null,
|
||||
beforeRequest,
|
||||
responded: { onSuccess, onError, onComplete },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// @/utils/alova-iview.ts
|
||||
import { createBschAlova } from "./alova-core";
|
||||
import { Message } from "view-ui-plus";
|
||||
import conf from "@/assets/config/host.config";
|
||||
|
||||
/* 全局 loading 遮罩实例 */
|
||||
let spinInstance: any = null;
|
||||
|
||||
/* 主动关闭 loading */
|
||||
function closeLoading() {
|
||||
if (spinInstance) {
|
||||
Message.destroy();
|
||||
spinInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
const baseUrl = conf.serviceHost;
|
||||
|
||||
/* 创建带 View Design UI 的 Alova 实例 */
|
||||
export const alovaInstance = createBschAlova(baseUrl, {
|
||||
injectHeaders(method) {
|
||||
// currentRole: encodeURI(JSON.stringify({ userId, roleId, schId: schoolId })),
|
||||
if (localStorage.getItem("currentRole") != null) {
|
||||
method.config.headers["currentRole"] = localStorage.getItem("currentRole") as string;
|
||||
}
|
||||
},
|
||||
|
||||
/* 2. 加载动画 – View Design Spin */
|
||||
onLoadingStart() {
|
||||
spinInstance = Message.loading("加载中...");
|
||||
},
|
||||
onLoadingEnd: closeLoading,
|
||||
onBizError(msg: string) {
|
||||
if (!!msg) Message.error(msg);
|
||||
},
|
||||
|
||||
onHttpError(msg: string) {
|
||||
Message.error({ title: "网络错误", content: msg });
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { App } from "vue";
|
||||
|
||||
import realImg from "./real-img";
|
||||
|
||||
// 自动导入当前文件夹下的所有自定义指令(默认导出项)
|
||||
export default (app: App<Element>) => {
|
||||
realImg(app);
|
||||
};
|
||||
|
||||
// const modules = import.meta.glob("../directive/**/**.ts");
|
||||
|
||||
// for (const path in modules) {
|
||||
// // 排除当前文件
|
||||
// if (path !== "../directive/index.ts") {
|
||||
// modules[path]().then(mod => {
|
||||
// mod.default(app);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { App } from "vue";
|
||||
|
||||
export default (app: App<Element>): void => {
|
||||
app.directive("real-img", {
|
||||
async mounted(el, binding) {
|
||||
console.log(el);
|
||||
if (el.getAttribute("data-load") == null) el.classList.add("img-hide");
|
||||
let imgURL = binding.value; //获取图片地址
|
||||
|
||||
if (imgURL) {
|
||||
let exist = await imageIsExist(imgURL);
|
||||
if (exist) {
|
||||
el.setAttribute("src", imgURL);
|
||||
el.setAttribute("data-load", "1");
|
||||
}
|
||||
}
|
||||
el.classList.remove("img-hide");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测图片是否存在
|
||||
* @param url
|
||||
*/
|
||||
let imageIsExist = function (url: string) {
|
||||
return new Promise(resolve => {
|
||||
var img: HTMLImageElement | null = new Image();
|
||||
img.onload = function () {
|
||||
if ((this as any).complete == true) {
|
||||
resolve(true);
|
||||
img = null;
|
||||
}
|
||||
};
|
||||
img.onerror = function () {
|
||||
resolve(false);
|
||||
img = null;
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
};
|
||||
@@ -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,326 @@
|
||||
import type { IObj } from "@/types/Basics";
|
||||
import { actionIcon, actionText } from "@/utils/tableActions";
|
||||
export default class Utils {
|
||||
/**
|
||||
* 格式化日期
|
||||
*
|
||||
* @param {(Date | string)} 日期
|
||||
* @param {string} fmt 格式化字符串
|
||||
* @returns {string} 日期字符串
|
||||
* @memberof Utils
|
||||
*/
|
||||
public static dateFormat(date: Date | string, fmt: string): string {
|
||||
if (typeof date === "string") date = date.replace(/\-/g, "/");
|
||||
let dt = new Date(date);
|
||||
let o: IObj = {
|
||||
"M+": dt.getMonth() + 1, //月份
|
||||
"d+": dt.getDate(), //日
|
||||
"H+": dt.getHours(), //小时
|
||||
"m+": dt.getMinutes(), //分
|
||||
"s+": dt.getSeconds(), //秒
|
||||
"q+": Math.floor((dt.getMonth() + 3) / 3), //季度
|
||||
S: dt.getMilliseconds(), //毫秒
|
||||
};
|
||||
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (dt.getFullYear() + "").substr(4 - RegExp.$1.length));
|
||||
|
||||
for (let k in o)
|
||||
if (new RegExp("(" + k + ")").test(fmt))
|
||||
fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length));
|
||||
return fmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取地址栏指定参数
|
||||
*/
|
||||
public static getUrlParam(name: string): string | null {
|
||||
let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
|
||||
let r = window.location.search.substr(1).match(reg);
|
||||
if (r != null) return unescape(r[2]);
|
||||
else return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成uuid
|
||||
*/
|
||||
public static uuid(): string {
|
||||
return "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||
let r = (Math.random() * 16) | 0,
|
||||
v = c == "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文件大小
|
||||
*/
|
||||
public static getFileSize(size: number): string {
|
||||
if (!size) return "";
|
||||
if (size / 1024 < 1024) {
|
||||
size = Math.ceil(size / 1024);
|
||||
return size + "KB";
|
||||
} else if (size / (1024 * 1024) < 1024) {
|
||||
size = Math.ceil((size / (1024 * 1024)) * 100) / 100;
|
||||
return size + "MB";
|
||||
} else if (size / (1024 * 1024 * 1024) < 1024) {
|
||||
size = Math.ceil((size / (1024 * 1024 * 1024)) * 100) / 100;
|
||||
return size + "GB";
|
||||
} else return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定键
|
||||
*/
|
||||
public static delKey(obj: IObj, ...args: string[]) {
|
||||
args.forEach(v => delete obj[v]);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* iview Tooltip封装(旧 API,薄封装转调 tableActions.actionIcon,签名不变)
|
||||
* 2026-10-10 起 D1 修正:禁用态 onClick 置 null(原先禁用仍可点)。
|
||||
*/
|
||||
public static operatingHandle(
|
||||
h: any,
|
||||
info: string,
|
||||
iconClass: string,
|
||||
callback: Function,
|
||||
disabled: boolean = false,
|
||||
) {
|
||||
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,
|
||||
className: string,
|
||||
callback: Function,
|
||||
disabled: boolean = false,
|
||||
) {
|
||||
return actionText(h, {
|
||||
key: info,
|
||||
label: info,
|
||||
danger: (className || "").includes("text-error"),
|
||||
on: () => callback(),
|
||||
disabled,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算表格高度
|
||||
*/
|
||||
public static useTableHeight({
|
||||
isSearch = false,
|
||||
isPage = false,
|
||||
isTabs = false,
|
||||
isOperat = false,
|
||||
otherH = 0,
|
||||
} = {}) {
|
||||
let bodyH = document.body.clientHeight,
|
||||
headerH = 112,
|
||||
searchH = 52,
|
||||
operatH = 48,
|
||||
tabsH = 56,
|
||||
pageH = 72,
|
||||
paddingH = 8;
|
||||
|
||||
try {
|
||||
//@ts-ignore 平台注入的全局对象,独立运行时不存在
|
||||
const bschUtil = BschLoadUtil.getInstance();
|
||||
if (!bschUtil.getIsSelf()) headerH = 56;
|
||||
} catch (e) {
|
||||
/* 独立运行:保留自身 header(56) + 面包屑(56) = 112 */
|
||||
}
|
||||
let tableH = bodyH - headerH - paddingH;
|
||||
isOperat && (tableH -= operatH);
|
||||
isTabs && (tableH -= tabsH);
|
||||
isSearch && (tableH -= searchH);
|
||||
isPage && (tableH -= pageH);
|
||||
otherH > 0 && (tableH -= otherH);
|
||||
return tableH;
|
||||
}
|
||||
/**
|
||||
* 数字转大写汉字
|
||||
* @param num
|
||||
* @returns
|
||||
*/
|
||||
public static convertToChinaNum(num: number) {
|
||||
var arr1 = new Array("零", "一", "二", "三", "四", "五", "六", "七", "八", "九");
|
||||
var arr2 = new Array(
|
||||
"",
|
||||
"十",
|
||||
"百",
|
||||
"千",
|
||||
"万",
|
||||
"十",
|
||||
"百",
|
||||
"千",
|
||||
"亿",
|
||||
"十",
|
||||
"百",
|
||||
"千",
|
||||
"万",
|
||||
"十",
|
||||
"百",
|
||||
"千",
|
||||
"亿",
|
||||
); //可继续追加更高位转换值
|
||||
if (!num || isNaN(num)) {
|
||||
return "零";
|
||||
}
|
||||
var english = num.toString().split("");
|
||||
var result = "";
|
||||
for (var i = 0; i < english.length; i++) {
|
||||
var des_i = english.length - 1 - i; //倒序排列设值
|
||||
result = arr2[i] + result;
|
||||
var arr1_index: any = english[des_i];
|
||||
result = arr1[arr1_index] + result;
|
||||
}
|
||||
//将【零千、零百】换成【零】 【十零】换成【十】
|
||||
result = result.replace(/零(千|百|十)/g, "零").replace(/十零/g, "十");
|
||||
//合并中间多个零为一个零
|
||||
result = result.replace(/零+/g, "零");
|
||||
//将【零亿】换成【亿】【零万】换成【万】
|
||||
result = result.replace(/零亿/g, "亿").replace(/零万/g, "万");
|
||||
//将【亿万】换成【亿】
|
||||
result = result.replace(/亿万/g, "亿");
|
||||
//移除末尾的零
|
||||
result = result.replace(/零+$/, "");
|
||||
//将【零一十】换成【零十】
|
||||
//result = result.replace(/零一十/g, '零十');//貌似正规读法是零一十
|
||||
//将【一十】换成【十】
|
||||
result = result.replace(/^一十/g, "十");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象转url参数
|
||||
* @param num
|
||||
* @returns
|
||||
*/
|
||||
public static createUrl(url: string, query: any) {
|
||||
var temp: any[] = [];
|
||||
for (var k in query) {
|
||||
if (query[k] != null) temp.push(k + "=" + query[k]);
|
||||
}
|
||||
return url + "?" + temp.join("&");
|
||||
}
|
||||
/**
|
||||
* 获取当前角色信息
|
||||
* @returns
|
||||
*/
|
||||
public static getCurrentRoleInfo<T>() {
|
||||
let currentRole = localStorage.getItem("currentRole");
|
||||
if (!currentRole) return null;
|
||||
let { schId, roleId, childId, userId } = JSON.parse(decodeURI(currentRole ?? ""));
|
||||
return { schId, roleId, childId, userId } as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* tree对象转化为数组
|
||||
* @param tree
|
||||
* @returns
|
||||
*/
|
||||
public static treeToArray(tree: any[]): any[] {
|
||||
return tree.reduce((res, item) => {
|
||||
const { children, ...i } = item;
|
||||
return res.concat(i, children && children.length ? this.treeToArray(children) : []);
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组转化为tree对象
|
||||
* @param tree
|
||||
* @returns
|
||||
*/
|
||||
public static arrayToTree(arr: any[], pid: number): any[] {
|
||||
return arr
|
||||
.filter(item => item.pid === pid)
|
||||
.map(item => ({ ...item, children: this.arrayToTree(arr, item.id) }));
|
||||
}
|
||||
|
||||
//字符串的16进制表达
|
||||
public static strToHexCharCode(_str: string) {
|
||||
if (_str === "") return "";
|
||||
var hexCharCode: string[] = [];
|
||||
for (var i = 0; i < _str.length; i++) {
|
||||
var str = _str.charCodeAt(i).toString(16);
|
||||
if (_str.length == 1) str += "0" + _str;
|
||||
hexCharCode.push(str);
|
||||
}
|
||||
return hexCharCode.join("");
|
||||
}
|
||||
|
||||
public static hexToString(hex: string) {
|
||||
if (hex === "") return "";
|
||||
// 处理奇数长度,补前导零
|
||||
if (hex.length % 2 !== 0) {
|
||||
hex = "0" + hex;
|
||||
[[3]];
|
||||
}
|
||||
let str = "";
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
// 每两位解析为一个字符的Unicode值
|
||||
const hexPair = hex.substr(i, 2);
|
||||
const charCode = parseInt(hexPair, 16);
|
||||
str += String.fromCharCode(charCode);
|
||||
[[14, 16]];
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将颜色值HEX格式转换为rgb的格式
|
||||
* @param {hex} hex 需要转换的rgb字符串
|
||||
* @param {number} opacity 透明度
|
||||
* @return {string} ;
|
||||
*/
|
||||
public static hexToRgb(hex: string, opacity: number = 1) {
|
||||
let str = hex.replace("#", "");
|
||||
if (str.length % 3) {
|
||||
return "hex格式不正确!";
|
||||
}
|
||||
//获取截取的字符长度
|
||||
let count = str.length / 3;
|
||||
//根据字符串的长度判断是否需要 进行幂次方
|
||||
let power = 6 / str.length;
|
||||
let r = parseInt("0x" + str.substring(0 * count, 1 * count)) ** power;
|
||||
let g = parseInt("0x" + str.substring(1 * count, 2 * count)) ** power;
|
||||
let b = parseInt("0x" + str.substring(2 * count)) ** power;
|
||||
return `rgb(${r}, ${g}, ${b},${opacity})`;
|
||||
}
|
||||
|
||||
public static removeHtmlTag(str: string) {
|
||||
str = str.replace(/<\/?[^>]*>/g, ""); //去除HTML tag
|
||||
str = str.replace(/[ | ]*\n/g, "\n"); //去除行尾空白
|
||||
str = str.replace(/\n[\s| | ]*\r/g, "\n"); //去除多余空行
|
||||
str = str.replace(/ /gi, ""); //去掉
|
||||
return str;
|
||||
}
|
||||
|
||||
public static unpackPerms = (bits: number) => [
|
||||
!!(bits & 1), // create
|
||||
!!(bits & 2), // update
|
||||
!!(bits & 4), // move
|
||||
!!(bits & 8), // delete
|
||||
];
|
||||
|
||||
public static copyToClipboard(text: string) {
|
||||
const input = document.createElement("input");
|
||||
input.setAttribute("value", text);
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
try {
|
||||
const successful = document.execCommand("copy");
|
||||
if (successful) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
} finally {
|
||||
document.body.removeChild(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 左导航渲染模型(GetUserMod 模块树 → 可渲染菜单树)。
|
||||
*
|
||||
* 纯函数、零运行时依赖(只 import type),解析器由调用方注入(resolveMenuPage)——
|
||||
* 便于在冒烟脚本里直接跑断言,不必起浏览器。
|
||||
*
|
||||
* 三条渲染规则(执行手册 §2.2「path 空行=纯权限锚点」的前端一侧):
|
||||
* ① <b>跳过判据取平台原始 path 是否为空</b>,不取解析结果:权限锚点(如 customId
|
||||
* fc:scope:all 的「数据全量」行)本就不进导航,靠名字/modId 猜出路由再显示反而是假象;
|
||||
* ② 有子节点的分组行即使自身 path 为空也保留为<b>分组头</b>(头本身不可点),
|
||||
* 子节点按 ① 过滤后全空则整个分组不渲染;
|
||||
* ③ 原始 path 非空但解析不出已注册路由的行<b>照常显示</b>,点击时提示配置错误——
|
||||
* 配置问题要暴露成可见告警,不能静默少一条菜单(与 utils/menuRoute.ts 的告警口径一致)。
|
||||
*/
|
||||
|
||||
import type { IModule } from "@/types/Basics";
|
||||
|
||||
/** 菜单项目标解析器(utils/menuRoute.ts 的 resolveMenuPage 同签名) */
|
||||
export type MenuResolver = (
|
||||
page?: string | null,
|
||||
modId?: number | string | null,
|
||||
name?: string | null
|
||||
) => { path: string; external: boolean };
|
||||
|
||||
/** 可渲染的导航节点 */
|
||||
export interface NavNode {
|
||||
id: number;
|
||||
modId: number | string;
|
||||
name: string;
|
||||
/** 平台配置的图标名(如 list),空串由 layout 兜底 icon-list */
|
||||
icon: string;
|
||||
/** 解析后的内部路由 path(外链为完整 URL);解析不出时为空串 */
|
||||
page: string;
|
||||
external: boolean;
|
||||
/** 平台原始配置串,仅排障用 */
|
||||
raw: string;
|
||||
/** 分组头(有可渲染子节点)为 true,本身不参与高亮/跳转 */
|
||||
group: boolean;
|
||||
children: NavNode[];
|
||||
}
|
||||
|
||||
/** 扁平项(面包屑与高亮对照用),pid=父节点 id(顶层为 0) */
|
||||
export interface FlatNav {
|
||||
id: number;
|
||||
name: string;
|
||||
modId: number | string;
|
||||
page: string;
|
||||
external: boolean;
|
||||
pid: number;
|
||||
}
|
||||
|
||||
/** 原始 path 是否为空(= 纯权限锚点,不进导航) */
|
||||
function isAnchorRow(node: IModule): boolean {
|
||||
return String(node?.page ?? "").trim() === "";
|
||||
}
|
||||
|
||||
export function buildNavTree(modList: IModule[] | null | undefined, resolve: MenuResolver): NavNode[] {
|
||||
const tree: NavNode[] = [];
|
||||
for (const node of modList ?? []) {
|
||||
if (!node) continue;
|
||||
const children = buildNavTree(node.children, resolve);
|
||||
const raw = String(node.page ?? "").trim();
|
||||
if (children.length) {
|
||||
// 规则②:分组头保留(自身 path 是否为空无关),子节点已按规则①过滤。
|
||||
// 分组头不作为链接渲染,page 只留作排障对照
|
||||
tree.push({
|
||||
id: node.id,
|
||||
modId: node.modId,
|
||||
name: node.name,
|
||||
icon: node.icon ?? "",
|
||||
page: resolve(raw, node.modId, node.name).path,
|
||||
external: false,
|
||||
raw,
|
||||
group: true,
|
||||
children
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// 规则①:叶子且原始 path 为空 = 权限锚点,跳过渲染
|
||||
if (isAnchorRow(node)) continue;
|
||||
const resolved = resolve(raw, node.modId, node.name);
|
||||
tree.push({
|
||||
id: node.id,
|
||||
modId: node.modId,
|
||||
name: node.name,
|
||||
icon: node.icon ?? "",
|
||||
page: resolved.path,
|
||||
external: resolved.external,
|
||||
raw,
|
||||
group: false,
|
||||
children: []
|
||||
});
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/** 深度优先展平(分组头不入表:它没有可跳转页面,高亮/面包屑只对照可跳转项) */
|
||||
export function flattenNav(tree: NavNode[], pid = 0): FlatNav[] {
|
||||
const out: FlatNav[] = [];
|
||||
for (const node of tree) {
|
||||
if (!node.group) {
|
||||
out.push({ id: node.id, name: node.name, modId: node.modId, page: node.page, external: node.external, pid });
|
||||
}
|
||||
if (node.children.length) out.push(...flattenNav(node.children, node.id));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 首个可跳转的菜单项 path(登录后默认落点;无则空串) */
|
||||
export function firstNavPage(tree: NavNode[]): string {
|
||||
for (const node of tree) {
|
||||
if (node.children.length) {
|
||||
const child = firstNavPage(node.children);
|
||||
if (child) return child;
|
||||
}
|
||||
if (!node.group && node.page && !node.external) return node.page;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** 可渲染项总数(分组头不计入) */
|
||||
export function countNavItems(tree: NavNode[]): number {
|
||||
return flattenNav(tree).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「单模块独占」= 只有一个可跳转菜单项且无分组:隐藏左导航、内容区铺满。
|
||||
* 无授权用户只余「我的填写」一行时即为此形态。
|
||||
*/
|
||||
export function isSingleNav(tree: NavNode[]): boolean {
|
||||
const hasGroup = tree.some((node) => node.group);
|
||||
return countNavItems(tree) <= 1 && !hasGroup;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import routes from "@/router/routes";
|
||||
|
||||
/**
|
||||
* 平台模块配置 → 前端内部路由的统一解析器(fork notice/front 的同名件,前缀换成本应用 ctx)。
|
||||
*
|
||||
* UserBase 里配置的模块 path 格式不可控(可能带 /form-center 前缀、带 index.html#/hash、
|
||||
* 是应用入口地址甚至完整 URL、或干脆配错),直接 router.push(item.page) 匹配不到就会白屏
|
||||
* 或被兜底路由吞掉。所有消费模块 page 的地方(侧边栏跳转/高亮、登录后默认跳转)必须先过这里。
|
||||
*
|
||||
* 解析顺序:路径精确匹配 → 模块名匹配 → 外链判定 → modId 契约匹配。
|
||||
* 全部落空时返回空结果并 console.warn,绝不返回未注册的原始路径。
|
||||
*/
|
||||
|
||||
/** 路由 path 集合:重定向路由穿透到目标 path;带参数的路由(/f/:code、/app/:appCode)不参与匹配 */
|
||||
const ROUTE_PATHS = routes
|
||||
.map((r) => (typeof r.redirect === "string" ? r.redirect : r.path))
|
||||
.filter((p): p is string => !!p && !p.includes(":"));
|
||||
|
||||
/** modId → 路由 path(取该 modId 的第一个非重定向路由;本应用路由表不写 modId,见 routes.ts 注②) */
|
||||
const ROUTE_BY_MODID = new Map<number, string>();
|
||||
for (const r of routes) {
|
||||
const modId = (r.meta as any)?.modId as number | undefined;
|
||||
if (!modId || !r.path || r.path.includes(":") || r.redirect) continue;
|
||||
if (!ROUTE_BY_MODID.has(modId)) ROUTE_BY_MODID.set(modId, r.path);
|
||||
}
|
||||
|
||||
/** 标题 → 路由 path(重复标题取先注册的) */
|
||||
const ROUTE_BY_TITLE = new Map<string, string>();
|
||||
for (const r of routes) {
|
||||
if (r.redirect || !r.path || r.path.includes(":")) continue;
|
||||
const title = (r.meta as any)?.title as string | undefined;
|
||||
if (title && !ROUTE_BY_TITLE.has(title)) ROUTE_BY_TITLE.set(title, r.path);
|
||||
}
|
||||
|
||||
/** 平台历史/近似模块名 → 路由(模块行名称与页面标题写法不一致时的对照) */
|
||||
const NAME_ALIASES: Record<string, string> = {
|
||||
表单设计: "/admin/forms",
|
||||
表单管理: "/admin/forms",
|
||||
数据管理: "/admin/data",
|
||||
答卷管理: "/admin/data",
|
||||
统计: "/admin/stats",
|
||||
统计分析: "/admin/stats",
|
||||
字典管理: "/admin/dicts",
|
||||
我的填写: "/my",
|
||||
我提交的: "/my",
|
||||
};
|
||||
|
||||
const EXTERNAL_RE = /^(https?:)?\/\//i;
|
||||
|
||||
/** 本应用上下文段(部署形态 /form-center/front/index.html#/…,模块行可能把 ctx 也配进 path) */
|
||||
const CTX = "form-center";
|
||||
|
||||
/** 从平台原始配置里提取内部路由候选串(去掉 hash、query、尾斜杠、入口文件名) */
|
||||
function extractCandidate(raw: string): string {
|
||||
let p = raw.trim();
|
||||
try {
|
||||
p = decodeURIComponent(p);
|
||||
} catch {
|
||||
/* 含非法编码时保持原样 */
|
||||
}
|
||||
// 带 hash 的地址(…/index.html#/admin/data):取 # 后的 SPA 路由
|
||||
const hashIdx = p.indexOf("#");
|
||||
if (hashIdx >= 0) {
|
||||
p = p.slice(hashIdx + 1);
|
||||
} else if (EXTERNAL_RE.test(p)) {
|
||||
// 无 hash 的完整 URL:取 pathname
|
||||
try {
|
||||
p = new URL(p, "http://placeholder.local").pathname;
|
||||
} catch {
|
||||
/* 保持原样 */
|
||||
}
|
||||
}
|
||||
const qIdx = p.indexOf("?");
|
||||
if (qIdx >= 0) p = p.slice(0, qIdx);
|
||||
// 入口文件名去掉后即可命中根路由
|
||||
p = p.replace(/index\.html?$/i, "");
|
||||
p = p.replace(/\/+$/, "");
|
||||
if (p && !p.startsWith("/")) p = "/" + p;
|
||||
return p;
|
||||
}
|
||||
|
||||
/** 候选串 → 已注册路由 path;未命中返回 null */
|
||||
function matchRoute(candidate: string): string | null {
|
||||
if (!candidate) return null;
|
||||
const lower = candidate.toLowerCase();
|
||||
// 1. 精确匹配(含重定向穿透后的目标路径)
|
||||
const exact = ROUTE_PATHS.find((p) => p.toLowerCase() === lower);
|
||||
if (exact) return exact;
|
||||
// 2. 兼容带 ctx 前缀的配置(/form-center/admin/data → /admin/data)
|
||||
// 不做通用后缀匹配,避免外部系统路径被误命中
|
||||
const withCtx = "/" + CTX + "/";
|
||||
if (lower.startsWith(withCtx)) {
|
||||
return ROUTE_PATHS.find((p) => p.toLowerCase() === lower.slice(withCtx.length - 1)) ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 按模块名解析路由:精确标题 → 别名 → 双向包含(命中多个则放弃) */
|
||||
function matchByName(name?: string | null): string | null {
|
||||
const n = String(name ?? "").trim();
|
||||
if (!n) return null;
|
||||
const byTitle = ROUTE_BY_TITLE.get(n);
|
||||
if (byTitle) return byTitle;
|
||||
const alias = NAME_ALIASES[n];
|
||||
if (alias) return alias;
|
||||
const hits = [...ROUTE_BY_TITLE.entries()].filter(
|
||||
([title]) => title.length >= 2 && (n.includes(title) || title.includes(n))
|
||||
);
|
||||
return hits.length === 1 ? hits[0][1] : null;
|
||||
}
|
||||
|
||||
/** 是否为本应用入口类地址(/、/form-center、…/index.html):只表示"打开应用",不携带页面信息 */
|
||||
function isAppEntryCandidate(candidate: string): boolean {
|
||||
return !candidate || candidate === "/" || candidate === "/" + CTX;
|
||||
}
|
||||
|
||||
export interface ResolvedMenuPage {
|
||||
/** 内部路由 path;解析不出时为空串 */
|
||||
path: string;
|
||||
/** 外部链接(完整 URL 且不属于本应用路由),需整页跳转而非 router.push */
|
||||
external: boolean;
|
||||
}
|
||||
|
||||
/** 空配置的统一返回值 */
|
||||
export const EMPTY_MENU_PAGE: ResolvedMenuPage = { path: "", external: false };
|
||||
|
||||
export function resolveMenuPage(
|
||||
page?: string | null,
|
||||
modId?: number | string | null,
|
||||
name?: string | null
|
||||
): ResolvedMenuPage {
|
||||
const raw = String(page ?? "").trim();
|
||||
const byName = (): string | null => matchByName(name);
|
||||
const byModId = (): string | null => (modId != null ? ROUTE_BY_MODID.get(modId as number) ?? null : null);
|
||||
|
||||
if (raw) {
|
||||
const candidate = extractCandidate(raw);
|
||||
const byPath = matchRoute(candidate);
|
||||
if (byPath) return { path: byPath, external: false };
|
||||
|
||||
// 完整 URL:指向本应用入口的不算有效配置(继续用名字/modId 识别),
|
||||
// 其余按外部链接整页打开
|
||||
if (EXTERNAL_RE.test(raw) && !raw.includes("#")) {
|
||||
if (!isAppEntryCandidate(candidate)) {
|
||||
return { path: raw, external: true };
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = byName() ?? byModId();
|
||||
if (fallback) return { path: fallback, external: false };
|
||||
|
||||
console.warn(
|
||||
"[menuRoute] 模块路径/名称/modId 均无法匹配前端路由,请检查基础平台模块配置:",
|
||||
raw,
|
||||
"modId =",
|
||||
modId,
|
||||
"name =",
|
||||
name
|
||||
);
|
||||
return EMPTY_MENU_PAGE;
|
||||
}
|
||||
|
||||
// 未配置 path:按模块名 → modId 契约解析(纯权限锚点行在这里就会落空——
|
||||
// 它本就不该进导航,跳过规则见 utils/menuNav.ts,不靠本函数判权限)
|
||||
const fallback = byName() ?? byModId();
|
||||
if (fallback) return { path: fallback, external: false };
|
||||
return EMPTY_MENU_PAGE;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// import { BschIframeType, LightAppModal, IframePageParmas, RoleInfo, StateEnum } from "@/types";
|
||||
import mitt, { Emitter } from "mitt";
|
||||
|
||||
type MittType = {
|
||||
reload: void; //刷新页面
|
||||
setMenuPath: number; //选择左侧菜单
|
||||
resetMenuPath: any[]; //重置设置左侧菜单
|
||||
setNavPath: any[]; //设置面包屑
|
||||
resetLayout: { identity: number; modId: number }; //重置页面菜单
|
||||
};
|
||||
|
||||
// 类型
|
||||
const emitter: Emitter<MittType> = mitt<MittType>();
|
||||
// 导出
|
||||
export default emitter;
|
||||
@@ -0,0 +1,166 @@
|
||||
// @ts-nocheck
|
||||
/* 本端跑 strict vue-tsc(npm run type-check 是 T5 起的门禁),四端同构源落本端的既有约定
|
||||
照 open-gateway/theme.ts 补 @ts-nocheck:正文与 UserBase/flow-center 同构源逐字节一致
|
||||
(MD5 973393684783c740399b8eb9e97369bc),仅顶部此二行 + 本注释为本端补丁。
|
||||
strict 下两处告警均无害——L16 的 h 被各渲染器形参遮蔽(导入冗余)、
|
||||
actionMore 的 it.on() 形参在 normalizeActions 已闭包绑定。 */
|
||||
/**
|
||||
* 表格操作列统一原子(方案: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),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
/**
|
||||
* 快应用上下文 /app/:appCode/* 占位(T5)。
|
||||
*
|
||||
* 形态(方案 §6.2 纯 A 形态、手册 §2.1):快应用没有独立前端,UserBase 应用行 pc_url 直接指向
|
||||
* 本路由,中心按<b>那个应用</b>的模块树渲染轻导航/直落单表单——取树走 api/module/base.ts 的
|
||||
* getUserMod(appCode)(参数名按 P2 定稿 appCode)。
|
||||
*
|
||||
* P3 裁决的口径在本页成立:<b>H4 未上线时该请求返回空树,页面就显示空态,绝不回落本应用菜单</b>
|
||||
* (回落会把「要 A 应用的菜单」伪装成「A 应用就这些」)。树渲染与范围矩阵在 T8 落地。
|
||||
*/
|
||||
const route = useRoute();
|
||||
const appCode = computed(() => String(route.params.appCode ?? ""));
|
||||
const view = computed(() => String(route.params.view ?? ""));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-wrapper yh-fill">
|
||||
<div class="main-container">
|
||||
<h4 class="subtitle">快应用上下文</h4>
|
||||
<Alert show-icon type="info">
|
||||
<code>/app/:appCode/*</code> 路由骨架占位;目标应用模块树渲染、单表单直落与应用聚合轻导航在 T8 落地。
|
||||
</Alert>
|
||||
<p class="app-meta">
|
||||
appCode = <b>{{ appCode || "(空)" }}</b>
|
||||
<template v-if="view">|view = <b>{{ view }}</b></template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-meta {
|
||||
margin: 16px 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 应用入口 /(T5)。
|
||||
* 壳在拿到授权模块树后会 replace 到第一个可跳转菜单项(App.vue),正常用户不会停在本页;
|
||||
* 停在这里只有两种情况:一个可跳转模块都没有(未授权 / 平台未配模块行),或外链型菜单。
|
||||
* 这里给的是可 diagnosis 的空态文案,不做任何本地兜底菜单。
|
||||
*/
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-wrapper yh-fill">
|
||||
<div class="main-container">
|
||||
<h4 class="subtitle">表单中心</h4>
|
||||
<Alert show-icon type="warning">
|
||||
当前账号在表单中心没有可进入的页面。请确认用户中心已为本应用配置模块行(手册 §2.2 表)并对所属组/角色授权;
|
||||
全员默认应可见「我的填写」。
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
/**
|
||||
* 填报页 /f/:code 占位(T5)。
|
||||
* 本体(code 寻址 + resolve、mode 闭集、控件渲染与校验、提交与幂等、ResizeObserver 高度上报)在 T7 落地。
|
||||
* 本轮只固定两件事:①路由形态与参数名;②self != top 的剥壳态可被肉眼确认(壳由 App.vue 判,
|
||||
* 这里只把它显示出来,方便冒烟时区分直航/嵌入两种形态——方案 §3.3 同一 URL 的两种形态)。
|
||||
*/
|
||||
const route = useRoute();
|
||||
const code = computed(() => String(route.params.code ?? ""));
|
||||
const embedded = window.self !== window.top;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 嵌入态不套 page-wrapper 骨架:剥壳后由页面自己铺满(T7 接高度上报) -->
|
||||
<div :class="embedded ? 'fill-plain' : 'page-wrapper yh-fill'">
|
||||
<div class="main-container">
|
||||
<h4 class="subtitle">填报</h4>
|
||||
<Alert show-icon type="info">
|
||||
<code>/f/:code</code> 路由骨架占位,表单寻址与答卷提交在 T7 落地。
|
||||
</Alert>
|
||||
<p class="fill-meta">
|
||||
code = <b>{{ code || "(空)" }}</b>
|
||||
|形态 = <b>{{ embedded ? "页面块(self != top,已剥壳)" : "整页(直航,带壳)" }}</b>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fill-plain {
|
||||
height: 100%;
|
||||
}
|
||||
.fill-meta {
|
||||
margin: 16px 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import PageStub from "@/components/PageStub.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageStub title="我的填写" stage="T8" note="全员授权行(手册 §2.2):我提交的/关于我的两视图在 T8 落地,两个视图不做参数分支。" />
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import PageStub from "@/components/PageStub.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageStub
|
||||
title="数据管理"
|
||||
stage="T8"
|
||||
note="两段式列表 + keyset(提交时间序)、可见列选择记忆、批量软删/改状态、按钉住版本编辑在 T8 落地;数据范围(scope)由后端裁决,前端只消费。"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import PageStub from "@/components/PageStub.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageStub
|
||||
title="字典管理"
|
||||
stage="T6"
|
||||
note="form_dict / form_dict_item 两级维护、条目 value 不可变、options.source=dict:{code} 引用在 T6 落地;平台级字典(sch_id=0)只在超管上下文维护(方案 §6.5)。"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import PageStub from "@/components/PageStub.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageStub
|
||||
title="表单设计"
|
||||
stage="T6"
|
||||
note="设计器三件套(14 类控件面板 + 属性面板 + 表单级配置)、发布/升版流程、另存为副本在 T6 落地。"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import PageStub from "@/components/PageStub.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageStub
|
||||
title="统计"
|
||||
stage="T8"
|
||||
note="总数/按状态/按字段分组走接口聚合(铁律④:前端不自己聚合),缓存失效仅升版;UserBase 侧「统计」行可并入数据管理,二选一。"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_BASE_HOST: string;
|
||||
readonly VITE_SSO_HOST: string;
|
||||
readonly VITE_SERVICE_HOST: string;
|
||||
/** true 时启用 vite-plugin-mock(npm run dev:mock),build 产物恒 undefined */
|
||||
readonly VITE_IS_MOCK: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
// vite 插件虚拟模块(vite-plugin-svg-icons / unocss)
|
||||
declare module "virtual:svg-icons-register";
|
||||
declare module "virtual:uno.css";
|
||||
@@ -1,18 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"types": ["vite/client", "node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
@@ -1,17 +1,141 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { ConfigEnv, UserConfig, defineConfig } from "vite";
|
||||
import { createHtmlPlugin } from "vite-plugin-html";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import path, { resolve } from "path";
|
||||
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
|
||||
import { viteMockServe } from "vite-plugin-mock";
|
||||
import ViteAutoImport from "unplugin-auto-import/vite";
|
||||
|
||||
// T1 只做工程壳(CI 的 npm ci + npm run build 必须过)。
|
||||
// T5 fork notice/front 时在此接入 A 类 loader、auto-import、svg icons、dev proxy 等平台约定。
|
||||
export default defineConfig({
|
||||
// 相对路径产物:静态资源在 projman 的 /form-center/ 挂载下也能取到(同 notice / flow-center 口径)
|
||||
base: "./",
|
||||
plugins: [vue()],
|
||||
build: {
|
||||
outDir: "dist"
|
||||
},
|
||||
server: {
|
||||
// 本地 dev 端口(后端 8086;8077=notice front、8087=svc-hall front、8088=flow-center front 顺延)
|
||||
port: 8089
|
||||
}
|
||||
import UnoCSS from "@unocss/vite";
|
||||
import presetUno from "@unocss/preset-uno";
|
||||
import presetRemToPx from "@unocss/preset-rem-to-px";
|
||||
|
||||
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||
// 构建产物直出 dist/(notice / flow-center 同口径):projman 前端发布要求 zip 根含 index.html,
|
||||
// 不得再套 dist/base 中间层;dev(serve) 不写产物,两者共用该值无冲突
|
||||
const outDir = "dist";
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
vue(),
|
||||
createSvgIconsPlugin({
|
||||
iconDirs: [resolve(process.cwd(), "src/assets/icons")],
|
||||
symbolId: "icon-[dir]-[name]",
|
||||
inject: "body-last",
|
||||
customDomId: "__svg__icons__dom__",
|
||||
}),
|
||||
viteMockServe({
|
||||
mockPath: "./src/assets/mock",
|
||||
// 仅 --mode mock 时启用 mock 中间件;dev/联调/正式均走真实后端
|
||||
localEnabled: mode === "mock",
|
||||
}),
|
||||
ViteAutoImport({
|
||||
imports: [
|
||||
"vue",
|
||||
"vue-router",
|
||||
{
|
||||
"view-ui-plus": ["Button", "Table", "Modal", "Message", "Form", "FormItem", "Input", "Select"],
|
||||
},
|
||||
{
|
||||
from: "alova/client",
|
||||
imports: ["usePagination", "useRequest", "useWatcher", "useForm"],
|
||||
},
|
||||
],
|
||||
dts: "./src/auto-imports.d.ts",
|
||||
dirs: ["src/api"], // 自动导入 api hooks
|
||||
}),
|
||||
createHtmlPlugin({
|
||||
minify: true,
|
||||
pages: [
|
||||
{
|
||||
entry: "src/main.ts",
|
||||
template: "index.html",
|
||||
filename: "index.html",
|
||||
injectOptions: {
|
||||
data: {
|
||||
title: "表单中心",
|
||||
injectScript: ``,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
UnoCSS({
|
||||
presets: [presetUno(), presetRemToPx({ baseFontSize: 4 })],
|
||||
shortcuts: {
|
||||
"flex-center": "flex items-center justify-center",
|
||||
btn: "px-16px py-8px rounded-4px cursor-pointer transition-all duration-200",
|
||||
card: "bg-white rounded-8px shadow-md p-16px",
|
||||
"text-ellipsis": "truncate",
|
||||
"text-multi-ellipsis": "line-clamp-2",
|
||||
},
|
||||
theme: {
|
||||
fontSize: {
|
||||
xs: "12px",
|
||||
sm: "14px",
|
||||
base: "16px",
|
||||
lg: "18px",
|
||||
xl: "20px",
|
||||
"2xl": "24px",
|
||||
},
|
||||
},
|
||||
}),
|
||||
// 主题接入已免插件化(A 类,规范 §2.1):唯一接入点 = index.html 的 loader;
|
||||
// 下面的 /static 代理只是 dev 联调便利(prod 同源用不到),删除不影响线上主题
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: "localhost",
|
||||
// 本地 dev 端口(执行手册 §5 P6:service=8086 / front=8089)
|
||||
port: 8089,
|
||||
proxy: {
|
||||
// dev 正式模式:serviceHost 为空(同源相对路径 /api/...),经此代理转发到本地后端
|
||||
// (后端 CAS 过滤器对 /api/* 不回 CORS 头,浏览器直连 8086 会被拦,必须同源代理)
|
||||
"/api": {
|
||||
target: "http://localhost:8086",
|
||||
changeOrigin: true,
|
||||
},
|
||||
// 主题通道本地联调:dev 下 loader 走同源相对,manifest/CSS 经此代理到中央通道。
|
||||
// 联调本地通道源:YschTheme 侧 npm run serve:static(默认 9527),再用
|
||||
// YH_THEME_PROXY_TARGET=http://localhost:9527 覆盖本 target
|
||||
"/static": {
|
||||
target: process.env.YH_THEME_PROXY_TARGET || "http://base.yunhaixingtong.com",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
base: "./",
|
||||
build: {
|
||||
target: "modules",
|
||||
outDir,
|
||||
assetsDir: "assets",
|
||||
sourcemap: false,
|
||||
minify: "esbuild",
|
||||
chunkSizeWarningLimit: 1000,
|
||||
cssTarget: "chrome61", // 安卓微信 webview 不支持 #RGBA 十六进制颜色
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve("index.html"),
|
||||
},
|
||||
output: {
|
||||
chunkFileNames: "static/js/[name]-[hash].js",
|
||||
entryFileNames: "static/js/[name]-[hash].js",
|
||||
assetFileNames: (assetInfo: any) => {
|
||||
if (assetInfo.type === "asset" && /\.(jpe?g|png|gif|svg)$/i.test(assetInfo.name)) {
|
||||
return "static/img/[name].[hash][ext]";
|
||||
}
|
||||
if (assetInfo.type === "asset" && /\.(ttf|woff|woff2|eot)$/i.test(assetInfo.name)) {
|
||||
return "static/fonts/[name].[hash][ext]";
|
||||
}
|
||||
return "static/[ext]/[name]-[hash].[ext]";
|
||||
},
|
||||
},
|
||||
manualEntrypoint: "index.html",
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||