@@ -0,0 +1,9 @@
|
|||||||
|
# 开发环境(vite dev --mode dev 加载;build 产物不读任何 .env.*)
|
||||||
|
# 问卷后端走 /api 同源相对路径(vite proxy → localhost:8090);
|
||||||
|
# FC 后端走 /fcapi 同源代理(→ localhost:8086,rewrite 去前缀);
|
||||||
|
# FC 前端(设计器/填写页 iframe src)直连其 dev 地址 localhost:8089
|
||||||
|
VITE_BASE_HOST=http://base.yunhaixingtong.com
|
||||||
|
VITE_SSO_HOST=http://sso.yunhaixingtong.com
|
||||||
|
VITE_SERVICE_HOST=
|
||||||
|
VITE_FC_FRONT=http://localhost:8089
|
||||||
|
VITE_IS_MOCK=false
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# scratch
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# vite 配置 bundle 临时产物
|
||||||
|
vite.config.*.timestamp-*
|
||||||
@@ -1,3 +1,38 @@
|
|||||||
# ysch-question-front
|
# 问卷前端 question-front(ysch-question-front)
|
||||||
|
|
||||||
managed by projman
|
问卷应用 R1「先行跑通」前端:Vue3 + TS + Vite5 + view-ui-plus 1.3.24(模板 Notice),端口 8091。链路:**新建问卷(自动挂接 FC 表单)→ iframe 内用 FC 设计器编辑 → 发布投放(链接+二维码)→ FC 填写页提交 → 回收明细与基础统计**。表单定义/版本/校验/答卷权威全在 form-center(FC),提交入口永远在 FC 填写页 `#/f/{code}`(本应用不建第二提交入口)。
|
||||||
|
|
||||||
|
配套后端:`ysch-question-service`(question-center,Spring Boot 2.7.18,端口 8090)。
|
||||||
|
|
||||||
|
## 前端路由(UserBase app_module 菜单契约)
|
||||||
|
|
||||||
|
modId 对应 app_module.custom_id(稳定契约;前端解析顺序=模块 path → 模块名 → modId,path 配错也有兜底):
|
||||||
|
|
||||||
|
| custom_id (modId) | 菜单名(建议=路由 title) | 模块 path | 角色 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | 问卷列表 | `/` | 管理端入口 |
|
||||||
|
| 2 | 新建问卷 | `/new` | 新建向导(子页,parentId=1) |
|
||||||
|
| 3 | 我的问卷 | `/my` | 教师/学生填写入口 |
|
||||||
|
| 4 | 未挂接表单 | `/orphans` | FC/问卷侧对账(子页,parentId=1) |
|
||||||
|
|
||||||
|
深层子页**不配菜单**:`/edit`(编辑容器)、`/publish`(发布投放)、`/answers`(回收明细)、`/stats`(基础统计),modId 契约 11~14,高亮挂父菜单 1。
|
||||||
|
|
||||||
|
## 启动(本地 dev)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --legacy-peer-deps # 本机 npm 10.8.2 需此参(arborist peer 解析 bug)
|
||||||
|
npm run dev # 8091;代理 /api→8090、/fcapi→8086、/static→主题通道
|
||||||
|
```
|
||||||
|
|
||||||
|
打开 http://localhost:8091 → 登录后见问卷菜单(平台 GetUserMod 未配置时自动用本地兜底菜单)。
|
||||||
|
|
||||||
|
## 测试与构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsc --noEmit # 类型检查(vue-tsc 1.8 本机崩溃勿用,以 tsc --noEmit 为准)
|
||||||
|
npm run build # vite build(dist/)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 红线(沿任务书 §7)
|
||||||
|
|
||||||
|
不迁旧数据;不做家长问卷;不跨校;不建第二答卷提交入口;不改 form-center/flow-center/svc-hall/Notice/UserBase/BasePlatform;不 import FC 在途未提交类;OAPI 只用已提交的 schema/answer/answers 三端点;`appCode` 恒为 `question`。
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
<!-- 运行时配置引导(Notice 同款):部署态同步拉 /<contextPath>/api/env-config;
|
||||||
|
本地开发(根路径,段数不足 2)不设置,走 .env.dev -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var seg = location.pathname.split("/").filter(Boolean);
|
||||||
|
if (seg.length < 2) return;
|
||||||
|
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 类免插件接入,Notice 同款):origin 零写死,四档优先级;
|
||||||
|
挂载/FOUC/CSS 排序全部由通道 init 脚本承担 -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "question-front",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"nodeversion": "20.19.4",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --mode dev",
|
||||||
|
"build": "vite build --mode base",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"alova": "^3.4.0",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
|
"pinia": "^2.1.4",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"sass": "^1.64.1",
|
||||||
|
"view-ui-plus": "1.3.24",
|
||||||
|
"vue": "^3.4.19",
|
||||||
|
"vue-router": "4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.4.4",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
|
"@unocss/preset-rem-to-px": "^66.6.0",
|
||||||
|
"@unocss/preset-uno": "^66.6.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"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 |
@@ -0,0 +1,90 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from "vue";
|
||||||
|
import layout from "./components/layout.vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import config from "@/assets/config/host.config";
|
||||||
|
import { Message } from "view-ui-plus";
|
||||||
|
import { baseApi } from "@/api/base";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const isLogin = ref(false);
|
||||||
|
const layoutData = reactive({
|
||||||
|
appName: "问卷管理",
|
||||||
|
baseHost: config.baseHost,
|
||||||
|
ssoHost: config.ssoHost,
|
||||||
|
appList: [] as any[],
|
||||||
|
userInfo: null as any,
|
||||||
|
modList: [] as any[],
|
||||||
|
isAlone: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// 1. 获取当前用户信息
|
||||||
|
const user: any = await baseApi.getUserInfo().send();
|
||||||
|
const { id, name, profilePicture } = user || {};
|
||||||
|
|
||||||
|
// 2. 获取当前用户已授权的模块列表(平台统一下发)
|
||||||
|
let modList: any[] = [];
|
||||||
|
try {
|
||||||
|
modList = (await baseApi.getUserMod().send()) || [];
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[question] GetUserMod 失败,使用本地菜单兜底", e);
|
||||||
|
const { getMenuList } = await import("@/assets/config/menu");
|
||||||
|
modList = await getMenuList();
|
||||||
|
}
|
||||||
|
if (!modList.length) {
|
||||||
|
const { getMenuList } = await import("@/assets/config/menu");
|
||||||
|
modList = await getMenuList();
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutData.userInfo = { id, name: name || "用户", avatar: profilePicture || "" };
|
||||||
|
layoutData.modList = modList;
|
||||||
|
|
||||||
|
const hasChildren = (modList || []).some((m: any) => m.children && m.children.length > 0);
|
||||||
|
layoutData.isAlone = (modList || []).length <= 1 && !hasChildren;
|
||||||
|
|
||||||
|
isLogin.value = true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("加载用户信息失败", error);
|
||||||
|
if (error?.message !== "redirect") {
|
||||||
|
Message.error("载入用户信息失败,请检查后端服务是否正常!");
|
||||||
|
}
|
||||||
|
layoutData.userInfo = { name: "用户", avatar: "" };
|
||||||
|
layoutData.modList = [];
|
||||||
|
layoutData.isAlone = true;
|
||||||
|
isLogin.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<layout v-if="isLogin" :layoutData="layoutData" />
|
||||||
|
<div v-if="isLogin" class="base-wrap-main" :class="{ 'alone-page': layoutData.isAlone }">
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<router-view :key="$route.fullPath" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.base-wrap-main .main-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.ivu-table-cell > .ivu-tooltip {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { get, ApiMethod } from "./http";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础端点(问卷后端 /api/base/*,Notice 同款形态)。
|
||||||
|
*/
|
||||||
|
const BASE = "/api/base";
|
||||||
|
|
||||||
|
export const baseApi = {
|
||||||
|
getUserInfo: (): ApiMethod<any> => get(`${BASE}/GetUserInfo`),
|
||||||
|
getUserMod: (): ApiMethod<any[]> => get(`${BASE}/GetUserMod`),
|
||||||
|
ping: (): ApiMethod<any> => get(`/api/question/ping`),
|
||||||
|
};
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { get, postJSON, ApiMethod } from "./http";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* form-center 前端直调接口(登录会话,credentials:"include";vite 代理 /fcapi → FC 后端 :8086,
|
||||||
|
* rewrite 去前缀——生产部署为同域 /form-center/api/...,由部署配置统一)。
|
||||||
|
* 只用 HEAD 已提交端点;零改动 form-center(任务书红线)。
|
||||||
|
*/
|
||||||
|
const FC = "/fcapi/api/form";
|
||||||
|
|
||||||
|
/** FC 管理端表单视图(FormModels.FormView 直出) */
|
||||||
|
export interface FcFormView {
|
||||||
|
formUuid: string;
|
||||||
|
schId: number;
|
||||||
|
appCode: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
status: number; // 0草稿 1已发布 2停用
|
||||||
|
statusName: "DRAFT" | "PUBLISHED" | "DISABLED";
|
||||||
|
currentVersion: number | null;
|
||||||
|
draftVersion: number | null;
|
||||||
|
audience: string[]; // STUDENT/TEACHER/ANONYMOUS
|
||||||
|
fillLimit: number; // 0不限/1限一次
|
||||||
|
deadline: string | null;
|
||||||
|
answerNoEnabled: number;
|
||||||
|
frozen: number;
|
||||||
|
anonCookieMinutes: number | null;
|
||||||
|
groupTag: string | null;
|
||||||
|
createTime: string;
|
||||||
|
updateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** FC fillable 行(/api/form/my/fillable) */
|
||||||
|
export interface FcFillableRow {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
appCode: string;
|
||||||
|
audience: string[];
|
||||||
|
deadline: string | null;
|
||||||
|
fillLimit: number;
|
||||||
|
groupTag: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FcConfigRequest {
|
||||||
|
uuid: string;
|
||||||
|
audience?: string[];
|
||||||
|
fillLimit?: number;
|
||||||
|
deadline?: string;
|
||||||
|
frozen?: number;
|
||||||
|
anonCookieMinutes?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fcApi = {
|
||||||
|
/** 新建三步·第一步:创建 FC 表单(appCode 恒 question;守卫=校管理员∪超管) */
|
||||||
|
create: (name: string, code?: string): ApiMethod<FcFormView> =>
|
||||||
|
postJSON<FcFormView>(`${FC}/admin/create`, { name, code, appCode: "question" }),
|
||||||
|
/** 新建三步·第二步:表单配置(问卷侧固定 frozen=1 + 受众/限次/截止) */
|
||||||
|
config: (body: FcConfigRequest): ApiMethod<FcFormView> => postJSON<FcFormView>(`${FC}/admin/config`, body),
|
||||||
|
/** 编辑容器打开时拉当前版本/发布态(含 name/audience/deadline 冗余) */
|
||||||
|
get: (uuid: string): ApiMethod<FcFormView> => get<FcFormView>(`${FC}/admin/get`, { uuid }),
|
||||||
|
/** 本校 question 应用的全部 FC 表单(未挂接对账页数据源) */
|
||||||
|
list: (): ApiMethod<FcFormView[]> => get<FcFormView[]>(`${FC}/admin/list`, { appCode: "question" }),
|
||||||
|
/** 我的可填清单(我的问卷交集数据源) */
|
||||||
|
fillable: (): ApiMethod<FcFillableRow[]> => get<FcFillableRow[]>(`${FC}/my/fillable`, { appCode: "question" }),
|
||||||
|
/** 复制问卷(FC copy:新 uuid、version 从 1、deadline 不继承) */
|
||||||
|
copy: (uuid: string, name: string): ApiMethod<FcFormView> =>
|
||||||
|
postJSON<FcFormView>(`${FC}/admin/copy`, { uuid, name }),
|
||||||
|
/** 结束投放可选项:FC 停用(硬门禁——断填报入口,数据可读可导) */
|
||||||
|
disable: (uuid: string): ApiMethod<void> =>
|
||||||
|
postJSON<void>(`${FC}/admin/disable?uuid=${encodeURIComponent(uuid)}`),
|
||||||
|
/** 孤儿表单清理(未挂接对账页;R1 提供按钮,调 FC 软删) */
|
||||||
|
softDelete: (uuid: string): ApiMethod<void> =>
|
||||||
|
postJSON<void>(`${FC}/admin/softDelete?uuid=${encodeURIComponent(uuid)}`),
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Method, AlovaGenerics } from "alova";
|
||||||
|
import { alovaInstance } from "@/utils/alova/alova-iview";
|
||||||
|
|
||||||
|
/** alova 3 Method 泛型便捷形态:R=Responded(响应解包后的 data 类型),其余槽位 any */
|
||||||
|
export type ApiMethod<T = any> = Method<AlovaGenerics<T>>;
|
||||||
|
|
||||||
|
/** GET 请求(信封解包由 alova-iview responded 钩子完成) */
|
||||||
|
export function get<T = any>(url: string, params?: Record<string, any>): ApiMethod<T> {
|
||||||
|
return new Method<AlovaGenerics<T>>("GET", alovaInstance as any, url, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST JSON body(与后端 @RequestBody 对齐) */
|
||||||
|
export function postJSON<T = any>(url: string, data?: Record<string, any> | any[]): ApiMethod<T> {
|
||||||
|
return new Method<AlovaGenerics<T>>("POST", alovaInstance as any, url, { data });
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { get, postJSON } from "./http";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 问卷后端接口(question-center /api/question/**,统一由 alova 解包 data)。
|
||||||
|
*/
|
||||||
|
const API = "/api/question";
|
||||||
|
|
||||||
|
/** 问卷行(后端 Questionnaire 实体直出) */
|
||||||
|
export interface QuestionnaireVo {
|
||||||
|
id: number;
|
||||||
|
schId: number;
|
||||||
|
formUuid: string;
|
||||||
|
formCode: string | null;
|
||||||
|
titleSnapshot: string | null;
|
||||||
|
formVersion: number | null;
|
||||||
|
status: number; // 0草稿 1收集中 2已结束
|
||||||
|
description: string | null;
|
||||||
|
anonymous: boolean;
|
||||||
|
publishTime: string | null;
|
||||||
|
finishTime: string | null;
|
||||||
|
createTime: string | null;
|
||||||
|
updateTime: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterRequest {
|
||||||
|
formUuid: string;
|
||||||
|
formCode?: string;
|
||||||
|
title: string;
|
||||||
|
anonymous?: boolean;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnswerRowVo {
|
||||||
|
id: number;
|
||||||
|
answerUuid: string;
|
||||||
|
formUuid: string;
|
||||||
|
valuesJson: string;
|
||||||
|
submitterId: number | null;
|
||||||
|
subjectId: number | null;
|
||||||
|
status: string | null;
|
||||||
|
isDeleted: boolean;
|
||||||
|
fcCreateTime: string | null;
|
||||||
|
fcUpdateTime: string | null;
|
||||||
|
pulledAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnswersPageVo {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
rows: AnswerRowVo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptionStatItem {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
count: number;
|
||||||
|
percent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldStatVo {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
caliber?: string;
|
||||||
|
answeredSheets?: number;
|
||||||
|
items: OptionStatItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextDetailVo {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
items: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatsVo {
|
||||||
|
total: number;
|
||||||
|
byStatus: { status: string; count: number }[];
|
||||||
|
byField: FieldStatVo[];
|
||||||
|
textDetail: TextDetailVo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const questionApi = {
|
||||||
|
list: () => get<QuestionnaireVo[]>(`${API}/questionnaire/list`),
|
||||||
|
register: (body: RegisterRequest) => postJSON<QuestionnaireVo>(`${API}/questionnaire`, body),
|
||||||
|
detail: (id: number) => get<QuestionnaireVo>(`${API}/questionnaire/${id}`),
|
||||||
|
refresh: (id: number) => get<QuestionnaireVo>(`${API}/questionnaire/${id}/refresh`),
|
||||||
|
updateDescription: (id: number, description: string) =>
|
||||||
|
postJSON<QuestionnaireVo>(`${API}/questionnaire/${id}/description`, { description }),
|
||||||
|
publish: (id: number) => postJSON<QuestionnaireVo>(`${API}/questionnaire/${id}/publish`),
|
||||||
|
finish: (id: number) => postJSON<QuestionnaireVo>(`${API}/questionnaire/${id}/finish`),
|
||||||
|
reopen: (id: number) => postJSON<QuestionnaireVo>(`${API}/questionnaire/${id}/reopen`),
|
||||||
|
softDelete: (id: number) => postJSON(`${API}/questionnaire/${id}/delete`),
|
||||||
|
myFillable: (codes: string[]) => postJSON<QuestionnaireVo[]>(`${API}/questionnaire/my`, { codes }),
|
||||||
|
answers: (id: number, page: number, pageSize: number) =>
|
||||||
|
get<AnswersPageVo>(`${API}/answers/${id}/list`, { page, pageSize }),
|
||||||
|
answerDetail: (id: number, answerUuid: string) =>
|
||||||
|
get(`${API}/answers/${id}/detail`, { answerUuid }),
|
||||||
|
stats: (id: number) => get<StatsVo>(`${API}/answers/${id}/stats`),
|
||||||
|
pull: (id: number) => postJSON<{ freshRows: number; totalActive: number }>(`${API}/answers/${id}/pull`),
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { BaseConf } from "@/types/Basics";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置优先级(Notice 同款三档):
|
||||||
|
* ① window.__APP_CONFIG__:线上运行时配置(index.html 引导脚本拉 env-config 写入)
|
||||||
|
* ② import.meta.env.VITE_*:仅本地 dev;build 产物恒 undefined
|
||||||
|
* ③ 同域推导兜底:/{contextPath}/front/ → serviceHost=/{contextPath};fcHost/fcFrontHost 同理
|
||||||
|
*/
|
||||||
|
const env = import.meta.env;
|
||||||
|
|
||||||
|
const seg = window.location.pathname.split("/").filter(Boolean);
|
||||||
|
const origin = window.location.origin;
|
||||||
|
|
||||||
|
let conf: BaseConf = {
|
||||||
|
baseHost: origin,
|
||||||
|
ssoHost: "",
|
||||||
|
serviceHost: seg.length >= 2 ? `${origin}/${seg[0]}` : "",
|
||||||
|
// FC 前端基址(设计器/填写页 iframe src 与投放链接拼接):
|
||||||
|
// 生产单域名多路径 → /form-center/front/#/f/{code};本地 dev 用 VITE_FC_FRONT=http://localhost:8089
|
||||||
|
fcFrontHost: seg.length >= 2 ? `${origin}/form-center/front` : "",
|
||||||
|
isMock: false,
|
||||||
|
isDebug: env.DEV,
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyConf = (target: any, source: any) => {
|
||||||
|
if (!source || typeof source !== "object") return;
|
||||||
|
Object.keys(source).forEach(key => {
|
||||||
|
if (source[key] !== undefined && source[key] !== null && source[key] !== "") {
|
||||||
|
(target as any)[key] = source[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
applyConf(conf, {
|
||||||
|
baseHost: env.VITE_BASE_HOST,
|
||||||
|
ssoHost: env.VITE_SSO_HOST,
|
||||||
|
serviceHost: env.VITE_SERVICE_HOST,
|
||||||
|
fcFrontHost: env.VITE_FC_FRONT,
|
||||||
|
isMock: env.VITE_IS_MOCK === "true",
|
||||||
|
});
|
||||||
|
|
||||||
|
const runtime = (window as any).__APP_CONFIG__;
|
||||||
|
applyConf(conf, runtime);
|
||||||
|
|
||||||
|
// HTTPS 自动升级
|
||||||
|
var ishttps = "https:" == document.location.protocol ? true : false;
|
||||||
|
if (ishttps) {
|
||||||
|
Object.keys(conf).forEach(element => {
|
||||||
|
if (typeof conf[element] === "string" && (conf[element] as string).indexOf("http://") > -1) {
|
||||||
|
conf[element] = (conf[element] as string).replace("http://", "https://");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default conf;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/** 平台模块行形态(GetUserMod 返回/本地兜底共用) */
|
||||||
|
export interface IModule {
|
||||||
|
id: number;
|
||||||
|
modId?: number;
|
||||||
|
name: string;
|
||||||
|
page: string;
|
||||||
|
children?: IModule[];
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地菜单兜底(平台 GetUserMod 未配置时的开发期形态;接入平台模块下发后以接口返回为准)。
|
||||||
|
*/
|
||||||
|
export function getMenuList(): Promise<IModule[]> {
|
||||||
|
const menu: IModule[] = [
|
||||||
|
{ id: 1, modId: 1, name: "问卷列表", page: "/" },
|
||||||
|
{ id: 2, modId: 2, name: "新建问卷", page: "/new" },
|
||||||
|
{ id: 3, modId: 3, name: "我的问卷", page: "/my" },
|
||||||
|
{ id: 4, modId: 4, name: "未挂接表单", page: "/orphans" },
|
||||||
|
];
|
||||||
|
return Promise.resolve(menu);
|
||||||
|
}
|
||||||
@@ -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,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,59 @@
|
|||||||
|
.rule-tree {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 280px !important;
|
||||||
|
.head-ul {
|
||||||
|
background: #f6f8f9;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
height: 48px;
|
||||||
|
li {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
.ivu-tooltip,
|
||||||
|
.ivu-tooltip-rel {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #eaeaea;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
&.disabled {
|
||||||
|
background-color: #fff;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #515a6e;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-box {
|
||||||
|
padding: 11px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ztree li span.button {
|
||||||
|
background-repeat: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,135 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-wrap {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
.left-box {
|
||||||
|
width: 200px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.user-list-ul {
|
||||||
|
height: calc(100% - 50px);
|
||||||
|
overflow-y: auto;
|
||||||
|
li {
|
||||||
|
height: 42px;
|
||||||
|
border: 1px solid var(--line, #e9edf5);
|
||||||
|
border-top: none;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border 200ms;
|
||||||
|
img {
|
||||||
|
object-fit: cover;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.stu-name {
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
span.stu-card {
|
||||||
|
width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
li.active {
|
||||||
|
// background: #f3f3f3;
|
||||||
|
color: var(--brand-2, #7b8ffd);
|
||||||
|
border-right: 3px solid var(--brand, #577afb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.right-box {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格加载(规范 §4 表格加载行):图形=通道 .ysch-spin(随 --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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复表格幻影省略号(2026-10-10):.ivu-table-cell 自带 overflow:hidden + text-overflow:ellipsis,
|
||||||
|
tooltip 列的包装层(.ivu-tooltip 行内块 + width:100%)未溢出也会被画出「...」;
|
||||||
|
恢复组件预期的 flex 层级即可,内层 .ivu-table-cell-tooltip-content 的真省略号与悬浮 tooltip 不受影响。
|
||||||
|
注意:不要再给操作列的裸 .ivu-tooltip 加 display:block——那会以更高特异性顶掉 App.vue 的横排规则,
|
||||||
|
使 operatingHandle 渲染的图标按钮竖排(2026-10-10 踩过,已回退)。 */
|
||||||
|
/* tooltip 列的包装层同时挂 .ivu-tooltip{display:inline-block},靠声明顺序顶掉了组件预期的 flex,这里恢复 */
|
||||||
|
.ivu-table .ivu-table-cell > .ivu-table-cell-tooltip {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
@@ -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,93 @@
|
|||||||
|
/* 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 baseApi: typeof import('./api/base')['baseApi']
|
||||||
|
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 fcApi: typeof import('./api/fc')['fcApi']
|
||||||
|
const get: typeof import('./api/http')['get']
|
||||||
|
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 postJSON: typeof import('./api/http')['postJSON']
|
||||||
|
const provide: typeof import('vue')['provide']
|
||||||
|
const questionApi: typeof import('./api/question')['questionApi']
|
||||||
|
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,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,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,303 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onBeforeMount, onBeforeUnmount, onMounted, watch, computed } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { Message } from "view-ui-plus";
|
||||||
|
import { resolveMenuPage } from "@/utils/menuRoute";
|
||||||
|
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 isPanel = ref(false);
|
||||||
|
const currPath = ref<string>("");
|
||||||
|
const navPath = ref<any[]>([]);
|
||||||
|
|
||||||
|
const baseHost = computed(() => props.layoutData?.baseHost || "");
|
||||||
|
const ssoHost = computed(() => props.layoutData?.ssoHost || "");
|
||||||
|
const appName = computed(() => props.layoutData?.appName || "通知公告");
|
||||||
|
const appList = computed(() => props.layoutData?.appList || []);
|
||||||
|
const userInfo = computed(() => props.layoutData?.userInfo || { name: "管理员", avatar: "" });
|
||||||
|
const isAlone = computed(() => props.layoutData?.isAlone ?? false);
|
||||||
|
const modList = computed(() => props.layoutData?.modList || []);
|
||||||
|
|
||||||
|
const pathNavStyle = computed(() => (isAlone.value ? { left: "0", width: "100%" } : null));
|
||||||
|
|
||||||
|
// 顶栏 Logo:localStorage["yh-logo-url"](平台键,yh-theme-init 异步补齐段写入)优先,本仓资产兜底——绝无空 src/裂图。
|
||||||
|
// 刷新:mounted 读一次 + 监听 yh-appearance-updated(服务端偏好到达后即时切换,无需刷新页面)。
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
readLogoUrl();
|
||||||
|
document.addEventListener("yh-appearance-updated", readLogoUrl);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => document.removeEventListener("yh-appearance-updated", readLogoUrl));
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
routeChange(route);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.fullPath,
|
||||||
|
() => routeChange(route)
|
||||||
|
);
|
||||||
|
|
||||||
|
const flaten = (tree: any[], pid = 0): any[] => {
|
||||||
|
const arr: any[] = [];
|
||||||
|
tree.forEach((element: any) => {
|
||||||
|
const { id, name, modId } = element;
|
||||||
|
// 平台配置的 path 格式不可控,统一解析为内部路由 path(名称/modId 兜底)
|
||||||
|
const resolved = resolveMenuPage(element.page, element.modId ?? element.id, element.name);
|
||||||
|
arr.push({ id, name, modId, page: resolved.path, external: resolved.external, pid });
|
||||||
|
if (element.children && element.children.length) {
|
||||||
|
arr.push(...flaten(element.children, id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuArr = computed(() => flaten(modList.value));
|
||||||
|
|
||||||
|
const routeChange = (to: any) => {
|
||||||
|
const meta: any = to.meta || {};
|
||||||
|
const { modId, title, parentId } = meta;
|
||||||
|
const navArr: any[] = [];
|
||||||
|
|
||||||
|
// 优先按 path 匹配菜单(平台模块 id 与路由 meta.modId 不保证一致),
|
||||||
|
// 匹配不到再退回 modId 对照
|
||||||
|
const find: any =
|
||||||
|
menuArr.value.find((x: any) => x.page && x.page === to.path) ||
|
||||||
|
menuArr.value.find((x: any) => x.modId === modId);
|
||||||
|
if (find) {
|
||||||
|
currPath.value = find.page || "";
|
||||||
|
if (find.pid) {
|
||||||
|
const pfind = menuArr.value.find((x: any) => x.id === find.pid);
|
||||||
|
if (pfind) navArr.push({ name: pfind.name });
|
||||||
|
}
|
||||||
|
navArr.push({ name: find.name || title, page: find.page });
|
||||||
|
navPath.value = navArr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentId) {
|
||||||
|
const parent = menuArr.value.find((x: any) => x.modId === parentId);
|
||||||
|
if (parent) {
|
||||||
|
currPath.value = parent.page || "";
|
||||||
|
if (parent.pid) {
|
||||||
|
const gp = menuArr.value.find((x: any) => 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 = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 统一解析菜单项目标:容忍平台配置的尾空格/旧 /notice 前缀/外链,返回内部路由 path 或外链 URL。
|
||||||
|
* 模板高亮与点击都走这里,避免「raw page(带尾空格)vs 解析后 path」不一致导致点不动/不高亮。 */
|
||||||
|
const resolveItemPage = (item: any) =>
|
||||||
|
resolveMenuPage(item?.page, item?.modId ?? item?.id, item?.name);
|
||||||
|
const pageOf = (item: any): string => resolveItemPage(item).path;
|
||||||
|
|
||||||
|
const linkMenu = (item: any) => {
|
||||||
|
const { path: page, external } = resolveItemPage(item);
|
||||||
|
if (!page) {
|
||||||
|
// 解析不出有效页面:显式提示,而不是静默不动或被重定向吞到默认页
|
||||||
|
Message.warning(`菜单「${item?.name || "未命名"}」未配置有效页面路径,请检查基础平台模块设置`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 平台配置的外部链接模块:整页跳转
|
||||||
|
if (external) {
|
||||||
|
window.location.href = page;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (page === route.path) {
|
||||||
|
router.go(0);
|
||||||
|
} else {
|
||||||
|
router.push(page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkUserInfo = () => {
|
||||||
|
// 头像/姓名点击统一跳 UserHome 设置页「个人信息」pane(默认头像轮 2026-09-30,原 /v/userinfo 已下线语义)
|
||||||
|
window.location.href = baseHost.value + "/userhome/front/index.html#/settings?pane=profile";
|
||||||
|
};
|
||||||
|
const logout = () => {
|
||||||
|
let url = window.location.href;
|
||||||
|
url = "/logout?service=" + url;
|
||||||
|
window.location.href = ssoHost.value + url;
|
||||||
|
};
|
||||||
|
const childChange = (index: number) => {
|
||||||
|
const arr = modList.value || [];
|
||||||
|
if (arr[index]) {
|
||||||
|
arr[index].childState = !arr[index].childState;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 菜单图标:读取用户中心模块管理配置的 YschIcons 图标名(GetUserMod 的 icon 字段,如 list → icon-list)。
|
||||||
|
* icons.css(ysch-icons 掩码版,基类 iconfont,类名 icon-<名称>)现由中央静态通道下发;
|
||||||
|
* 平台未配置图标时统一兜底 icon-list(2026-10-09 定稿:菜单项恒有图标,保整列缩进/行高一致) */
|
||||||
|
const getMenuIcon = (item?: any): string =>
|
||||||
|
`icon-${String(item?.icon || "").trim() || "list"}`;
|
||||||
|
|
||||||
|
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 徽标(结构与 Cms SchoolLayout 对齐,皮肤见通道 base.layout.css .logo-badge) -->
|
||||||
|
<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>
|
||||||
|
<!-- 配色切换器已移除(oapi 代理轮 2026-10-08 拍板):设置入口收敛在 UserHome 设置中心,
|
||||||
|
本地切换会被服务端生效值收敛打回,不留半残入口 -->
|
||||||
|
</header>
|
||||||
|
<div class="left-menu" v-if="!isAlone">
|
||||||
|
<div class="menu-top-box"></div>
|
||||||
|
<ul>
|
||||||
|
<template v-for="(item, index) in modList" :key="'menu_li_' + index">
|
||||||
|
<template v-if="item.children && item.children.length > 0">
|
||||||
|
<li>
|
||||||
|
<a href="javascript:;" @click="childChange(index)">
|
||||||
|
<i :class="item.childState ? '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="item.childState">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<transition name="fade_apps">
|
||||||
|
<div class="app-panel" v-if="isPanel">
|
||||||
|
<div class="tips-box"></div>
|
||||||
|
<ul class="fix">
|
||||||
|
<li class="fl" v-for="(item, index) in appList" :key="index">
|
||||||
|
<a :href="item.url" target="_blank" rel="noopener noreferrer">
|
||||||
|
<div class="icon-box">
|
||||||
|
<img :src="item.icon" alt="" />
|
||||||
|
</div>
|
||||||
|
<p class="text-ellipsis">{{ item.name }}</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="link-panel">
|
||||||
|
<a :href="baseHost + '/userhome/front/index.html#/apps'" target="_blank" rel="noopener noreferrer">
|
||||||
|
全部应用
|
||||||
|
<i>→</i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 仅覆盖/补充必要的动态样式,其余样式由中央静态通道的 base.layout.css(骨架 + 皮肤)提供 */
|
||||||
|
/* 菜单图标(通道下发的 ysch-icons 掩码类)与文字间距 */
|
||||||
|
.left-menu a .iconfont {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
/* 顶栏已由通道皮肤改为 flex 垂直居中(skin.src.css .header-container),
|
||||||
|
这里不再用 float 时代的 13px 顶偏移补偿,仅留右侧间距;徽标大小/圆底/阴影属皮肤 */
|
||||||
|
.header-container .logo-badge {
|
||||||
|
margin: 0 10px 0 0;
|
||||||
|
}
|
||||||
|
/* 面包屑「首页」垂直居中:骨架遗留的 .path-nav .icon-category{position:relative;top:1px}
|
||||||
|
是给老 iconfont 字体字形做的基线补偿,换成 ysch mask 的 1em 方块后反而把图标顶偏;
|
||||||
|
这里用 inline-flex 真居中并关掉该补偿(不动通道产物,覆盖收在本组件内) */
|
||||||
|
/* 面包屑「首页」与后续级目的文字对齐(2026-10-09 定稿):此前 .crumb-home 用 inline-flex,
|
||||||
|
行内级盒的基线取首项——16px 无字图标(其基线=方块底缘),整盒被顶高约 4px,首页文字
|
||||||
|
比相邻级目高。改回普通行内流:图标(.iconfont 基类 inline-block,基线=底缘)以 -0.125em
|
||||||
|
基线偏移做光学居中,文字与相邻级目共用同一行盒基线,天然对齐 */
|
||||||
|
.crumb-home .iconfont {
|
||||||
|
position: static;
|
||||||
|
top: 0;
|
||||||
|
margin-right: 4px;
|
||||||
|
vertical-align: -0.125em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
|
||||||
|
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)由中央静态通道下发
|
||||||
|
|
||||||
|
import "virtual:uno.css";
|
||||||
|
import "virtual:svg-icons-register";
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
app.mount("#app");
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createRouter, createWebHashHistory } from "vue-router";
|
||||||
|
import routes from "./routes";
|
||||||
|
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes,
|
||||||
|
});
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 问卷应用路由(扁平短路由,Notice 同款形态;hash 路由)。
|
||||||
|
* modId 是菜单 ↔ 路由的稳定契约(对照 UserBase app_module.custom_id,联调时以平台配置为准):
|
||||||
|
* 1=问卷列表 2=新建问卷 3=我的问卷 4=未挂接对账
|
||||||
|
* 深层子页不在菜单树,靠 meta.parentId 关联高亮父菜单:
|
||||||
|
* /edit=11(父1) /publish=12(父1) /answers=13(父1) /stats=14(父1)
|
||||||
|
*/
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
// 问卷列表(管理端入口)
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "question_list",
|
||||||
|
component: () => import("@/views/question/List.vue"),
|
||||||
|
meta: { title: "问卷列表", modId: 1 },
|
||||||
|
},
|
||||||
|
// 新建向导(列表的深层子页)
|
||||||
|
{
|
||||||
|
path: "/new",
|
||||||
|
name: "question_new",
|
||||||
|
component: () => import("@/views/question/Wizard.vue"),
|
||||||
|
meta: { title: "新建问卷", modId: 2, parentId: 1 },
|
||||||
|
},
|
||||||
|
// 我的问卷(教师/学生可填入口)
|
||||||
|
{
|
||||||
|
path: "/my",
|
||||||
|
name: "question_my",
|
||||||
|
component: () => import("@/views/question/MyFill.vue"),
|
||||||
|
meta: { title: "我的问卷", modId: 3 },
|
||||||
|
},
|
||||||
|
// 未挂接表单对账(只读展示 FC 有而问卷侧无绑定的孤儿)
|
||||||
|
{
|
||||||
|
path: "/orphans",
|
||||||
|
name: "question_orphans",
|
||||||
|
component: () => import("@/views/question/Orphans.vue"),
|
||||||
|
meta: { title: "未挂接表单", modId: 4, parentId: 1 },
|
||||||
|
},
|
||||||
|
// 编辑容器(iframe FC 设计器)
|
||||||
|
{
|
||||||
|
path: "/edit",
|
||||||
|
name: "question_edit",
|
||||||
|
component: () => import("@/views/question/Editor.vue"),
|
||||||
|
meta: { title: "编辑问卷", modId: 11, parentId: 1 },
|
||||||
|
},
|
||||||
|
// 发布投放(配置受众/截止/限次→FC config;生成链接/二维码)
|
||||||
|
{
|
||||||
|
path: "/publish",
|
||||||
|
name: "question_publish",
|
||||||
|
component: () => import("@/views/question/Publish.vue"),
|
||||||
|
meta: { title: "发布投放", modId: 12, parentId: 1 },
|
||||||
|
},
|
||||||
|
// 回收明细(分页答卷+单份查看+手动拉投影)
|
||||||
|
{
|
||||||
|
path: "/answers",
|
||||||
|
name: "question_answers",
|
||||||
|
component: () => import("@/views/question/Answers.vue"),
|
||||||
|
meta: { title: "回收明细", modId: 13, parentId: 1 },
|
||||||
|
},
|
||||||
|
// 基础统计(回收量/选项分布/文本明细)
|
||||||
|
{
|
||||||
|
path: "/stats",
|
||||||
|
name: "question_stats",
|
||||||
|
component: () => import("@/views/question/Stats.vue"),
|
||||||
|
meta: { title: "基础统计", modId: 14, parentId: 1 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
export const useDefaultStore = defineStore("defaultStore", {
|
||||||
|
state: () => ({
|
||||||
|
userRole: "school" as string,
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setUserRole(role: string) {
|
||||||
|
this.userRole = role;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* 公共类型
|
||||||
|
*/
|
||||||
|
|
||||||
|
export declare interface IObj {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare interface PageParams {
|
||||||
|
page: !number;
|
||||||
|
count: !number;
|
||||||
|
[keys: string]: any; //任意属性
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare interface PageData {
|
||||||
|
list: T;
|
||||||
|
total: number;
|
||||||
|
[keys: string]: any; //任意属性
|
||||||
|
}
|
||||||
|
interface RelayConf {
|
||||||
|
ziyuankuHost: string; //资源库
|
||||||
|
zhuanxiangHost: string; //专项
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 系统配置 */
|
||||||
|
export interface BaseConf {
|
||||||
|
baseHost: string;
|
||||||
|
ssoHost: string;
|
||||||
|
serviceHost: string;
|
||||||
|
relayConf?: RelayConf;
|
||||||
|
isMock: Boolean;
|
||||||
|
isDebug?: Boolean;
|
||||||
|
[keys: string]: any; //任意属性
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PC端角色 */
|
||||||
|
export interface PcDebugUser {
|
||||||
|
DebugUser: string;
|
||||||
|
AppType: string;
|
||||||
|
// yschapirequest: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* webapp端角色 */
|
||||||
|
export declare interface WappDebugUser {
|
||||||
|
ticket: string;
|
||||||
|
schoolId: Number;
|
||||||
|
userId: Number;
|
||||||
|
roleId: Number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare interface CurrentRole {
|
||||||
|
childId: number | null;
|
||||||
|
roleId: number;
|
||||||
|
schId: number;
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface layoutData {
|
||||||
|
appName: String;
|
||||||
|
appList: string[];
|
||||||
|
modList: Array<string>;
|
||||||
|
userInfo: Object;
|
||||||
|
ssoHost: String;
|
||||||
|
baseHost: String;
|
||||||
|
isAlone: Boolean; //是否只有一个选项卡
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseDict {
|
||||||
|
k: any;
|
||||||
|
v: string | null;
|
||||||
|
[keys: string]: any; //任意属性
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 计算表格高度的参数类型 */
|
||||||
|
export interface UseTableSetting {
|
||||||
|
/** 是否有查询面板 */
|
||||||
|
isSearch: boolean = true;
|
||||||
|
/** 是否有分页 */
|
||||||
|
isPage: boolean = true;
|
||||||
|
/** 是否是tabs布局 */
|
||||||
|
isTabs: boolean = false;
|
||||||
|
/** 是否有按钮操作 */
|
||||||
|
isOperat: boolean = true;
|
||||||
|
/** 额外的高度 */
|
||||||
|
otherH: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 布局组件类型 */
|
||||||
|
export interface LayoutData {
|
||||||
|
appName: string | null;
|
||||||
|
appList: Array<any>;
|
||||||
|
modList: Array<any>;
|
||||||
|
userInfo: any;
|
||||||
|
ssoHost: string | null;
|
||||||
|
baseHost: string | null;
|
||||||
|
isAlone: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义弹出对话框 */
|
||||||
|
export interface MyConfirm {
|
||||||
|
title?: string;
|
||||||
|
msg: string;
|
||||||
|
info?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
confirmText?: string;
|
||||||
|
closeText?: string;
|
||||||
|
onOk?: Function;
|
||||||
|
onCancel?: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare enum NODE_ENV {
|
||||||
|
/**
|
||||||
|
*开发环境
|
||||||
|
*/
|
||||||
|
development = "development",
|
||||||
|
/**
|
||||||
|
*生产环境
|
||||||
|
*/
|
||||||
|
production = "production",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const _: any;
|
||||||
|
export const $: any;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
const $: any;
|
||||||
|
const _: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
import Utils from "@/utils/index";
|
||||||
|
import mittBus from '@/utils/mitt'
|
||||||
|
import MyConfirm from "@/components/MyConfirm";
|
||||||
|
|
||||||
|
export interface VueGlobal {
|
||||||
|
$conf: BaseConf;
|
||||||
|
$bus: typeof mittBus;
|
||||||
|
$utils: typeof Utils;
|
||||||
|
$echarts: echarts;
|
||||||
|
[key:string]:any;
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { BaseDict } from "./Basics";
|
||||||
|
|
||||||
|
export enum OperatorEnum {
|
||||||
|
add,
|
||||||
|
del,
|
||||||
|
edit,
|
||||||
|
sel,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UserRoleEnum {
|
||||||
|
SuperAdmin = 1,
|
||||||
|
School,
|
||||||
|
}
|
||||||
|
|
||||||
|
//学段
|
||||||
|
export enum SchoolStage {
|
||||||
|
PRIMARY = 2,
|
||||||
|
MIDDLE,
|
||||||
|
HIGH,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SchoolStageDict: BaseDict[] = [
|
||||||
|
{
|
||||||
|
k: SchoolStage.PRIMARY,
|
||||||
|
v: "小学",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: SchoolStage.MIDDLE,
|
||||||
|
v: "初中",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: SchoolStage.HIGH,
|
||||||
|
v: "高中",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export enum RootNode {
|
||||||
|
/** 资源 */
|
||||||
|
ZIYUANKU = 1,
|
||||||
|
/** 辅导 */
|
||||||
|
TUTORING = 2,
|
||||||
|
/** 题库 */
|
||||||
|
QUESTION = 3,
|
||||||
|
/** 试卷 */
|
||||||
|
SHIJUAN = 4,
|
||||||
|
/** 专项 */
|
||||||
|
ZHUANXIANG = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RootNodeDict: BaseDict[] = [
|
||||||
|
{
|
||||||
|
k: RootNode.ZIYUANKU,
|
||||||
|
v: "资源",
|
||||||
|
path: "ziyuan",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: RootNode.TUTORING,
|
||||||
|
v: "辅导",
|
||||||
|
path: "fudao",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: RootNode.QUESTION,
|
||||||
|
v: "题库",
|
||||||
|
path: "tiku",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: RootNode.SHIJUAN,
|
||||||
|
v: "试卷",
|
||||||
|
path: "shijuan",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: RootNode.ZHUANXIANG,
|
||||||
|
v: "专项",
|
||||||
|
path: "zhuanxiang",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export enum BooxType {
|
||||||
|
/** 教材 */
|
||||||
|
TextBook = 1,
|
||||||
|
/** 教辅 */
|
||||||
|
TeachingTutoring,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BooxTypeDict: BaseDict[] = [
|
||||||
|
{
|
||||||
|
k: BooxType.TextBook,
|
||||||
|
v: "教材",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: BooxType.TeachingTutoring,
|
||||||
|
v: "教辅",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
//学校学段学制
|
||||||
|
export interface SchLenLevel {
|
||||||
|
stage: SchoolStage;
|
||||||
|
name: string;
|
||||||
|
grades: LenGrade[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LenGrade {
|
||||||
|
gradeId: number;
|
||||||
|
gradeName: string;
|
||||||
|
stage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AcademicYear {
|
||||||
|
year: number;
|
||||||
|
semester: number | null;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subject {
|
||||||
|
id: number;
|
||||||
|
tenantId: number;
|
||||||
|
baseId: null;
|
||||||
|
creatorId: number;
|
||||||
|
createDate: string;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
orderNum: number;
|
||||||
|
alias: string;
|
||||||
|
stage: number;
|
||||||
|
enable: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1 积分详情
|
||||||
|
* 2 分数详情
|
||||||
|
*/
|
||||||
|
export type ScoreModalType = 1 | 2;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* @description 扩展ruoter-meta的类型 此处必须要export {} 不然找不到类型
|
||||||
|
*/
|
||||||
|
declare module "vue-router" {
|
||||||
|
interface RouteMeta {
|
||||||
|
title?: string;
|
||||||
|
icon?: string;
|
||||||
|
modId?: number;
|
||||||
|
parentId?: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
declare module "*.vue" {
|
||||||
|
import { DefineComponent } from "vue";
|
||||||
|
const component: DefineComponent<{}, {}, any>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
@@ -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,347 @@
|
|||||||
|
import { IObj } from "@/types/Basics";
|
||||||
|
import { resolveComponent } from "vue";
|
||||||
|
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封装
|
||||||
|
*/
|
||||||
|
public static operatingHandle(
|
||||||
|
h: any,
|
||||||
|
info: string,
|
||||||
|
iconClass: string,
|
||||||
|
callback: Function,
|
||||||
|
disabled: boolean = false,
|
||||||
|
) {
|
||||||
|
if (disabled) iconClass += " disabled";
|
||||||
|
|
||||||
|
return h(
|
||||||
|
resolveComponent("Tooltip"),
|
||||||
|
{
|
||||||
|
content: info,
|
||||||
|
placement: "bottom",
|
||||||
|
transfer: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => [
|
||||||
|
h(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
class: `operating-btn iconfont ${iconClass}`,
|
||||||
|
onClick: disabled ? null : callback,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static operatingTextHandle(
|
||||||
|
h: any,
|
||||||
|
info: string,
|
||||||
|
className: string,
|
||||||
|
callback: Function,
|
||||||
|
disabled: boolean = false,
|
||||||
|
) {
|
||||||
|
if (disabled) className += " disabled";
|
||||||
|
return h(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
class: `operating-link ${className}`,
|
||||||
|
onClick: disabled ? null : callback,
|
||||||
|
},
|
||||||
|
info,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算表格高度
|
||||||
|
*/
|
||||||
|
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,160 @@
|
|||||||
|
import routes from "@/router/routes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台模块配置 → 前端内部路由的统一解析器。
|
||||||
|
*
|
||||||
|
* 基础平台里配置的模块 path 格式不可控(可能带 /notice 旧前缀、带
|
||||||
|
* index.html#/hash、是应用入口地址甚至完整 URL、或干脆配错),直接
|
||||||
|
* router.push(item.page) 匹配不到就会白屏或被重定向兜到默认页。所有消费
|
||||||
|
* 模块 page 的地方(侧边栏跳转/高亮、登录后默认跳转)必须先经过这里。
|
||||||
|
*
|
||||||
|
* 解析顺序:路径精确匹配 → 模块名匹配 → 外链判定 → modId 契约匹配。
|
||||||
|
* 全部落空时返回空结果并 console.warn,绝不返回未注册的原始路径——
|
||||||
|
* 把它 push 给路由器会被 / 重定向吞掉,表象就是"点了菜单跳到默认页"。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 路由 path 集合:重定向路由穿透到目标 path,保证解析结果可直接用于高亮对照 */
|
||||||
|
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 的第一个非重定向路由) */
|
||||||
|
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(重复标题取先注册的,如 发通知 → /list/new) */
|
||||||
|
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> = {
|
||||||
|
问卷列表: "/",
|
||||||
|
新建问卷: "/new",
|
||||||
|
我的问卷: "/my",
|
||||||
|
};
|
||||||
|
|
||||||
|
const EXTERNAL_RE = /^(https?:)?\/\//i;
|
||||||
|
|
||||||
|
/** 从平台原始配置里提取内部路由候选串(去掉 hash、query、尾斜杠) */
|
||||||
|
function extractCandidate(raw: string): string {
|
||||||
|
let p = raw.trim();
|
||||||
|
try {
|
||||||
|
p = decodeURIComponent(p);
|
||||||
|
} catch {
|
||||||
|
/* 含非法编码时保持原样 */
|
||||||
|
}
|
||||||
|
// 带 hash 的地址(.../index.html#/setting):取 # 后的 SPA 路由
|
||||||
|
const hashIdx = p.indexOf("#");
|
||||||
|
if (hashIdx >= 0) {
|
||||||
|
p = p.slice(hashIdx + 1);
|
||||||
|
} else if (EXTERNAL_RE.test(p)) {
|
||||||
|
// 无 hash 的完整 URL:取 pathname(如 https://host/setting)
|
||||||
|
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. 兼容旧 /notice 前缀(/notice/setting → /setting)。
|
||||||
|
// 不做通用后缀匹配,避免 /stats/OA/Notice 这类外部路径被误匹配
|
||||||
|
if (lower.startsWith("/question/")) {
|
||||||
|
const stripped = lower.slice("/question".length);
|
||||||
|
return ROUTE_PATHS.find((p) => p.toLowerCase() === stripped) ?? 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否为本应用入口类地址(/、/notice、…/index.html):只表示"打开应用",
|
||||||
|
* 不携带页面信息,不能当作有效跳转目标 */
|
||||||
|
function isAppEntryCandidate(candidate: string): boolean {
|
||||||
|
return !candidate || candidate === "/" || candidate === "/question";
|
||||||
|
}
|
||||||
|
|
||||||
|
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 | 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) ?? 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 契约解析
|
||||||
|
const fallback = byName() ?? byModId();
|
||||||
|
if (fallback) return { path: fallback, external: false };
|
||||||
|
console.warn("[menuRoute] 模块未配置路径且名称/modId 无法识别:", name, "modId =", modId);
|
||||||
|
return EMPTY_MENU_PAGE;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import mitt, { Emitter } from "mitt";
|
||||||
|
|
||||||
|
type MittEvents = {
|
||||||
|
reload: void; //刷新页面
|
||||||
|
setMenuPath: number; //选择左侧菜单
|
||||||
|
resetMenuPath: any[]; //重置设置左侧菜单
|
||||||
|
setNavPath: any[]; //设置面包屑
|
||||||
|
resetLayout: { identity: number; modId: number }; //重置页面菜单
|
||||||
|
};
|
||||||
|
|
||||||
|
// 事件总线
|
||||||
|
const emitter: Emitter<MittEvents> = mitt<MittEvents>();
|
||||||
|
// 导出
|
||||||
|
export default emitter;
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { Message } from "view-ui-plus";
|
||||||
|
import { questionApi, AnswerRowVo, QuestionnaireVo } from "@/api/question";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回收明细:分页答卷投影(未删行)+ 单份查看(values 原文 × schema 字段定义渲染)+ 手动拉投影。
|
||||||
|
* 定时投影默认 30s 一轮,此处「立即拉取」为即时补充。
|
||||||
|
*/
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const id = computed(() => Number(route.query.id || 0));
|
||||||
|
|
||||||
|
const q = ref<QuestionnaireVo | null>(null);
|
||||||
|
const rows = ref<AnswerRowVo[]>([]);
|
||||||
|
const total = ref(0);
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = 10;
|
||||||
|
const loading = ref(false);
|
||||||
|
const pulling = ref(false);
|
||||||
|
|
||||||
|
const detailVisible = ref(false);
|
||||||
|
const detailFields = ref<any[]>([]);
|
||||||
|
const detailValues = ref<Record<string, any>>({});
|
||||||
|
const detailMeta = ref<AnswerRowVo | null>(null);
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await questionApi.answers(id.value, page.value, pageSize).send();
|
||||||
|
rows.value = res.rows || [];
|
||||||
|
total.value = res.total || 0;
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "加载失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadQuestionnaire = async () => {
|
||||||
|
try {
|
||||||
|
q.value = await questionApi.detail(id.value).send();
|
||||||
|
} catch {
|
||||||
|
/* 列表页仍可用 */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pullNow = async () => {
|
||||||
|
pulling.value = true;
|
||||||
|
try {
|
||||||
|
const res = await questionApi.pull(id.value).send();
|
||||||
|
Message.success(`拉取完成:新 ${res.freshRows} 行,当前回收 ${res.totalActive} 份`);
|
||||||
|
load();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "拉取失败(form-center 不可达或问卷未发布)");
|
||||||
|
} finally {
|
||||||
|
pulling.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDetail = async (row: AnswerRowVo) => {
|
||||||
|
try {
|
||||||
|
const res: any = await questionApi.answerDetail(id.value, row.answerUuid).send();
|
||||||
|
detailMeta.value = res.row;
|
||||||
|
detailFields.value = flattenFields(res.fields);
|
||||||
|
detailValues.value = JSON.parse(res.row.valuesJson || "{}");
|
||||||
|
detailVisible.value = true;
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "加载答卷失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenFields = (groups: any[]): any[] => {
|
||||||
|
const out: any[] = [];
|
||||||
|
for (const g of groups || []) {
|
||||||
|
for (const f of g.fields || []) {
|
||||||
|
out.push(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 按字段类型渲染值(options 按 value→label;多值顿号连接) */
|
||||||
|
const renderValue = (field: any): string => {
|
||||||
|
const raw = detailValues.value[field.key];
|
||||||
|
if (raw == null) return "—";
|
||||||
|
const labelOf = (v: any): string => {
|
||||||
|
const opt = (field.options || []).find((o: any) => o.value === String(v));
|
||||||
|
return opt ? opt.label : String(v);
|
||||||
|
};
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
return raw.length ? raw.map(labelOf).join("、") : "—";
|
||||||
|
}
|
||||||
|
return labelOf(raw);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePage = (p: number) => {
|
||||||
|
page.value = p;
|
||||||
|
load();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadQuestionnaire();
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="bar">
|
||||||
|
<Button @click="router.push('/')">← 返回列表</Button>
|
||||||
|
<span class="title">回收明细{{ q ? `:${q.titleSnapshot}` : "" }}</span>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<Button :loading="pulling" @click="pullNow">立即拉取</Button>
|
||||||
|
<Button type="primary" ghost style="margin-left: 8px" @click="router.push({ path: '/stats', query: { id } })">
|
||||||
|
查看统计</Button>
|
||||||
|
</div>
|
||||||
|
<Table :loading="loading" :data="rows" :columns="[
|
||||||
|
{ title: '答卷号', key: 'answerUuid', width: 280, tooltip: true },
|
||||||
|
{ title: '提交人', key: 'submitterId', width: 100, slot: 'submitter' },
|
||||||
|
{ title: '状态', key: 'status', width: 110 },
|
||||||
|
{ title: '提交时间(FC)', key: 'fcCreateTime', width: 170 },
|
||||||
|
{ title: '投影时间', key: 'pulledAt', width: 170 },
|
||||||
|
{ title: '操作', slot: 'action', width: 110 },
|
||||||
|
]">
|
||||||
|
<template #submitter="{ row }">
|
||||||
|
{{ row.submitterId === 0 ? '匿名' : (row.submitterId ?? '—') }}
|
||||||
|
</template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<Button size="small" @click="openDetail(row)">查看</Button>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<div style="margin-top: 12px; text-align: right">
|
||||||
|
<Page :total="total" :model-value="page" :page-size="pageSize" show-total @on-change="changePage" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal v-model="detailVisible" title="答卷详情" width="640" footer-hide>
|
||||||
|
<Descriptions v-if="detailMeta" :column="2" size="small" border style="margin-bottom: 12px">
|
||||||
|
<DescriptionsItem label="答卷号">{{ detailMeta.answerUuid }}</DescriptionsItem>
|
||||||
|
<DescriptionsItem label="提交人">{{ detailMeta.submitterId === 0 ? '匿名' : (detailMeta.submitterId ?? '—') }}</DescriptionsItem>
|
||||||
|
<DescriptionsItem label="状态">{{ detailMeta.status || '—' }}</DescriptionsItem>
|
||||||
|
<DescriptionsItem label="提交时间">{{ detailMeta.fcCreateTime || '—' }}</DescriptionsItem>
|
||||||
|
</Descriptions>
|
||||||
|
<div v-for="f in detailFields" :key="f.key" class="kv">
|
||||||
|
<span class="k">{{ f.label }}{{ f.required ? ' *' : '' }}</span>
|
||||||
|
<span class="v">{{ renderValue(f) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="!detailFields.length" class="hint">(无 schema 字段——问卷未钉版或 FC schema 为空,values 原文:
|
||||||
|
{{ detailMeta?.valuesJson }})</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.kv {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 4px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.k {
|
||||||
|
width: 200px;
|
||||||
|
color: #666;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.v {
|
||||||
|
flex: 1;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { Message, Modal } from "view-ui-plus";
|
||||||
|
import config from "@/assets/config/host.config";
|
||||||
|
import { questionApi, QuestionnaireVo } from "@/api/question";
|
||||||
|
import { fcApi, FcFormView } from "@/api/fc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑容器:iframe 嵌 FC 设计器(FC App.vue 检测 self!==top 自动剥壳——已提交能力,零 FC 改动)。
|
||||||
|
* FC 不回发保存事件(TASK_2 才加),父页提供"我已保存完毕→刷新状态"按钮;
|
||||||
|
* 打开时拉 FC /admin/get 显示当前版本/发布态。FC 设计器地址:{FC前端}/#/admin/forms/designer?uuid=
|
||||||
|
*/
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const id = computed(() => Number(route.query.id || 0));
|
||||||
|
|
||||||
|
const q = ref<QuestionnaireVo | null>(null);
|
||||||
|
const fcForm = ref<FcFormView | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const designerSrc = computed(() =>
|
||||||
|
q.value ? `${config.fcFrontHost}/#/admin/forms/designer?uuid=${q.value.formUuid}` : "");
|
||||||
|
|
||||||
|
const fcStatusText = computed(() => {
|
||||||
|
if (!fcForm.value) return "未知";
|
||||||
|
if (fcForm.value.statusName === "PUBLISHED") return `已发布 v${fcForm.value.currentVersion}`;
|
||||||
|
if (fcForm.value.statusName === "DISABLED") return "已停用";
|
||||||
|
return fcForm.value.draftVersion ? `草稿 v${fcForm.value.draftVersion}` : "草稿(未保存过)";
|
||||||
|
});
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
q.value = await questionApi.refresh(id.value).send();
|
||||||
|
fcForm.value = await fcApi.get(q.value.formUuid).send();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "加载失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 我已保存完毕 → 拉最新版本/发布态(FC 不回发事件的折中) */
|
||||||
|
const confirmSaved = async () => {
|
||||||
|
await load();
|
||||||
|
Message.success(`状态已刷新:${fcStatusText.value}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goPublish = () => router.push({ path: "/publish", query: { id: id.value } });
|
||||||
|
|
||||||
|
/** 提示:题目编辑/保存/预览/发布都在 FC 设计器自带工具条完成 */
|
||||||
|
onMounted(() => {
|
||||||
|
Modal.info({
|
||||||
|
title: "编辑器使用说明",
|
||||||
|
content: "题目在下方 FC 设计器中编辑:拖入字段 → 保存 → 预览。保存/发布均在设计器工具条内完成;保存完毕后点击右上角「我已保存完毕」刷新状态。",
|
||||||
|
okText: "知道了",
|
||||||
|
});
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="bar">
|
||||||
|
<Button @click="router.push('/')">← 返回列表</Button>
|
||||||
|
<span class="title">{{ q?.titleSnapshot || "编辑问卷" }}</span>
|
||||||
|
<Tag v-if="q?.status === 0" color="default">草稿</Tag>
|
||||||
|
<Tag v-else-if="q?.status === 1" color="success">收集中</Tag>
|
||||||
|
<Tag v-else color="warning">已结束</Tag>
|
||||||
|
<span class="fc-status">FC 表单:{{ fcStatusText }}</span>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<Button :loading="loading" @click="confirmSaved">我已保存完毕,刷新状态</Button>
|
||||||
|
<Button type="primary" ghost style="margin-left: 8px" @click="goPublish">去发布投放</Button>
|
||||||
|
</div>
|
||||||
|
<div class="iframe-box" v-if="designerSrc">
|
||||||
|
<iframe :src="designerSrc" class="designer" frameborder="0" allow="fullscreen"></iframe>
|
||||||
|
</div>
|
||||||
|
<Spin v-else size="large" fix style="position: relative; height: 400px"></Spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding: 12px;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.fc-status {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.iframe-box {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
border: 1px solid #e8eaec;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.designer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { Message, Modal } from "view-ui-plus";
|
||||||
|
import { questionApi, QuestionnaireVo } from "@/api/question";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(false);
|
||||||
|
const rows = ref<QuestionnaireVo[]>([]);
|
||||||
|
|
||||||
|
const statusText = (s: number) => (s === 0 ? "草稿" : s === 1 ? "收集中" : "已结束");
|
||||||
|
const statusColor = (s: number) => (s === 0 ? "default" : s === 1 ? "success" : "warning");
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
rows.value = (await questionApi.list().send()) || [];
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toEdit = (row: QuestionnaireVo) => router.push({ path: "/edit", query: { id: row.id } });
|
||||||
|
const toPublish = (row: QuestionnaireVo) => router.push({ path: "/publish", query: { id: row.id } });
|
||||||
|
const toAnswers = (row: QuestionnaireVo) => router.push({ path: "/answers", query: { id: row.id } });
|
||||||
|
const toStats = (row: QuestionnaireVo) => router.push({ path: "/stats", query: { id: row.id } });
|
||||||
|
|
||||||
|
/** 有回收量再进编辑 → "建议复制新问卷"软引导(已拍板) */
|
||||||
|
const enterEdit = async (row: QuestionnaireVo) => {
|
||||||
|
try {
|
||||||
|
const page = await questionApi.answers(row.id, 1, 1).send();
|
||||||
|
if (page && page.total > 0) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "该问卷已有回收答卷",
|
||||||
|
content: "继续编辑会在 FC 侧升版(新版本行),已回收答卷不受影响;若只是想再发一轮,建议「复制新问卷」避免口径混淆。是否继续编辑?",
|
||||||
|
okText: "继续编辑",
|
||||||
|
cancelText: "取消",
|
||||||
|
onOk: () => toEdit(row),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 回收量查询失败不阻塞编辑入口 */
|
||||||
|
}
|
||||||
|
toEdit(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyQuestionnaire = async (row: QuestionnaireVo) => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
// FC copy(新 uuid、version 从 1、deadline 不继承)→ 登记问卷侧新行 → 进编辑
|
||||||
|
const { fcApi } = await import("@/api/fc");
|
||||||
|
const copied = await fcApi.copy(row.formUuid, `${row.titleSnapshot || "问卷"}-副本`).send();
|
||||||
|
const registered = await questionApi.register({
|
||||||
|
formUuid: copied.formUuid,
|
||||||
|
formCode: copied.code,
|
||||||
|
title: copied.name,
|
||||||
|
anonymous: row.anonymous,
|
||||||
|
}).send();
|
||||||
|
Message.success("复制成功,已进入新问卷编辑");
|
||||||
|
router.push({ path: "/edit", query: { id: registered.id } });
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "复制失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeQuestionnaire = (row: QuestionnaireVo) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "删除确认",
|
||||||
|
content: `删除问卷「${row.titleSnapshot || row.formCode}」?仅移除问卷侧记录,FC 表单与答卷数据不动(可在"未挂接表单"页看到)。`,
|
||||||
|
onOk: async () => {
|
||||||
|
await questionApi.softDelete(row.id).send();
|
||||||
|
Message.success("已删除");
|
||||||
|
load();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(load);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="toolbar">
|
||||||
|
<Button type="primary" @click="router.push('/new')">新建问卷</Button>
|
||||||
|
<Button @click="router.push('/my')">我的问卷</Button>
|
||||||
|
<Button @click="router.push('/orphans')">未挂接表单</Button>
|
||||||
|
<Button :loading="loading" @click="load">刷新</Button>
|
||||||
|
</div>
|
||||||
|
<Table :loading="loading" :data="rows" :columns="[
|
||||||
|
{ title: 'ID', key: 'id', width: 70 },
|
||||||
|
{ title: '问卷标题', key: 'titleSnapshot', tooltip: true },
|
||||||
|
{ title: 'FC code', key: 'formCode', width: 150 },
|
||||||
|
{ title: '受众', key: 'anonymous', width: 90, slot: 'anonymous' },
|
||||||
|
{ title: '状态', key: 'status', width: 100, slot: 'status' },
|
||||||
|
{ title: '发布版本', key: 'formVersion', width: 90, slot: 'version' },
|
||||||
|
{ title: '创建时间', key: 'createTime', width: 170 },
|
||||||
|
{ title: '操作', slot: 'action', width: 330 },
|
||||||
|
]">
|
||||||
|
<template #anonymous="{ row }">
|
||||||
|
<Tag :color="row.anonymous ? 'purple' : 'blue'">{{ row.anonymous ? '匿名' : '实名' }}</Tag>
|
||||||
|
</template>
|
||||||
|
<template #status="{ row }">
|
||||||
|
<Tag :color="statusColor(row.status)">{{ statusText(row.status) }}</Tag>
|
||||||
|
</template>
|
||||||
|
<template #version="{ row }">
|
||||||
|
{{ row.formVersion != null ? 'v' + row.formVersion : '—' }}
|
||||||
|
</template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<Button size="small" @click="enterEdit(row)">编辑</Button>
|
||||||
|
<Button size="small" type="primary" ghost style="margin-left: 6px" @click="toPublish(row)">投放</Button>
|
||||||
|
<Button size="small" style="margin-left: 6px" @click="toAnswers(row)">回收明细</Button>
|
||||||
|
<Button size="small" style="margin-left: 6px" @click="toStats(row)">统计</Button>
|
||||||
|
<Button size="small" style="margin-left: 6px" @click="copyQuestionnaire(row)">复制</Button>
|
||||||
|
<Button size="small" type="error" ghost style="margin-left: 6px" @click="removeQuestionnaire(row)">删除</Button>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||||
|
import { Message, Modal } from "view-ui-plus";
|
||||||
|
import config from "@/assets/config/host.config";
|
||||||
|
import { questionApi, QuestionnaireVo } from "@/api/question";
|
||||||
|
import { fcApi, FcFillableRow } from "@/api/fc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的问卷:FC fillable(appCode=question) ∩ 问卷 status=1。
|
||||||
|
* 点击进 FC 填写页(新窗或本页 iframe 模式);embed 消息 proto==="fc"&&v===1,origin 按"同 hostname"
|
||||||
|
* 放行(localhost 各端口共享 Cookie 域,任务书 §3.1)——formSubmitted 后刷新列表。
|
||||||
|
* 提交门禁(受众/截止/限次/匿名)全部由 FC 把关,问卷侧零旁路。
|
||||||
|
*/
|
||||||
|
const rows = ref<QuestionnaireVo[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const filling = ref<QuestionnaireVo | null>(null);
|
||||||
|
|
||||||
|
const fillUrl = computed(() =>
|
||||||
|
filling.value?.formCode ? `${config.fcFrontHost}/#/f/${filling.value.formCode}?mode=embed` : "");
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const fillable: FcFillableRow[] = (await fcApi.fillable().send()) || [];
|
||||||
|
const codes = fillable.map((r) => r.code);
|
||||||
|
rows.value = (await questionApi.myFillable(codes).send()) || [];
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "加载失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** embed 消息监听(FC utils/embed.ts 契约:proto:"fc", v:1;同 hostname 放行) */
|
||||||
|
const onMessage = (event: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const sameHost = new URL(event.origin).hostname === window.location.hostname;
|
||||||
|
if (!sameHost) return;
|
||||||
|
const data: any = event.data;
|
||||||
|
if (!data || data.proto !== "fc" || data.v !== 1) return;
|
||||||
|
if (data.type === "success" || data.type === "formSubmitted") {
|
||||||
|
Message.success("提交成功,感谢填写!");
|
||||||
|
closeFill();
|
||||||
|
load();
|
||||||
|
} else if (data.type === "needLogin") {
|
||||||
|
Message.warning("登录已过期,请刷新页面重新登录");
|
||||||
|
} else if (data.type === "error") {
|
||||||
|
Message.error(`提交失败:${data.error || "请检查必填项"}`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 非法 origin 忽略 */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openFill = (row: QuestionnaireVo) => {
|
||||||
|
filling.value = row;
|
||||||
|
};
|
||||||
|
const closeFill = () => {
|
||||||
|
filling.value = null;
|
||||||
|
};
|
||||||
|
const openNewWindow = (row: QuestionnaireVo) => {
|
||||||
|
if (!row.formCode) return;
|
||||||
|
window.open(`${config.fcFrontHost}/#/f/${row.formCode}`, "_blank");
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener("message", onMessage);
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => window.removeEventListener("message", onMessage));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="toolbar">
|
||||||
|
<span class="title">我的问卷</span>
|
||||||
|
<span class="hint">仅显示面向您所在人群、收集中且未过截止的问卷</span>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<Button :loading="loading" @click="load">刷新</Button>
|
||||||
|
</div>
|
||||||
|
<div class="cards">
|
||||||
|
<Card v-for="row in rows" :key="row.id" dis-hover class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<span class="name">{{ row.titleSnapshot }}</span>
|
||||||
|
<Tag v-if="row.anonymous" color="purple">匿名</Tag>
|
||||||
|
<Tag v-else color="blue">实名</Tag>
|
||||||
|
</div>
|
||||||
|
<p class="meta">发布于 {{ row.publishTime || "—" }}</p>
|
||||||
|
<div style="margin-top: 10px">
|
||||||
|
<Button type="primary" size="small" @click="openFill(row)">去填写</Button>
|
||||||
|
<Button size="small" style="margin-left: 6px" @click="openNewWindow(row)">新窗口打开</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<div v-if="!loading && !rows.length" class="empty">暂无可填写的问卷</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal v-model="filling" :title="filling?.titleSnapshot || '填写问卷'" fullscreen footer-hide
|
||||||
|
@on-visible-change="(v: boolean) => !v && closeFill()">
|
||||||
|
<div v-if="filling" class="fill-frame">
|
||||||
|
<iframe :src="fillUrl" frameborder="0" class="fill-iframe"></iframe>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 6px 0 0;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
color: #999;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
.fill-frame {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.fill-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { Message, Modal } from "view-ui-plus";
|
||||||
|
import { questionApi } from "@/api/question";
|
||||||
|
import { fcApi, FcFormView } from "@/api/fc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未挂接表单对账页(任务书 §4.1):FC 侧 appCode=question 的表单 ∖ 问卷侧已绑定 form_uuid。
|
||||||
|
* 只读展示 + 可选清理按钮(调 FC softDelete);第三步登记失败留下的孤儿在此补登记或清理。
|
||||||
|
*/
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(false);
|
||||||
|
const orphans = ref<FcFormView[]>([]);
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const fcForms: FcFormView[] = (await fcApi.list().send()) || [];
|
||||||
|
const bound = (await questionApi.list().send()) || [];
|
||||||
|
const boundUuids = new Set(bound.map((q) => q.formUuid));
|
||||||
|
orphans.value = fcForms.filter((f) => !boundUuids.has(f.formUuid));
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "加载失败(需要校管理员权限访问 FC 表单列表)");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 补登记:FC 表单已有而问卷侧没行(如向导第三步失败后遗留) */
|
||||||
|
const register = async (row: FcFormView) => {
|
||||||
|
try {
|
||||||
|
await questionApi.register({
|
||||||
|
formUuid: row.formUuid,
|
||||||
|
formCode: row.code,
|
||||||
|
title: row.name,
|
||||||
|
anonymous: (row.audience || []).includes("ANONYMOUS"),
|
||||||
|
}).send();
|
||||||
|
Message.success("已登记绑定");
|
||||||
|
load();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "登记失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 清理:调 FC 软删(唯一删除形态;FC 侧执行,问卷侧零改动) */
|
||||||
|
const clean = (row: FcFormView) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "清理未挂接表单",
|
||||||
|
content: `将软删 FC 表单「${row.name}(${row.code})」。该表单若有答卷数据仍可导出(FC 纪律:软删不断数据面)。确定清理?`,
|
||||||
|
okText: "清理(FC 软删)",
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await fcApi.softDelete(row.formUuid).send();
|
||||||
|
Message.success("已在 FC 侧软删");
|
||||||
|
load();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "清理失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusText = (f: FcFormView) =>
|
||||||
|
f.statusName === "PUBLISHED" ? `已发布 v${f.currentVersion}` : f.statusName === "DISABLED" ? "已停用" : "草稿";
|
||||||
|
|
||||||
|
onMounted(load);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="bar">
|
||||||
|
<Button @click="router.push('/')">← 返回列表</Button>
|
||||||
|
<span class="title">未挂接表单(对账)</span>
|
||||||
|
<span class="hint">FC 侧 question 应用表单中未被问卷绑定的孤儿;来源:新建向导第三步失败后未重试</span>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<Button :loading="loading" @click="load">刷新</Button>
|
||||||
|
</div>
|
||||||
|
<Table :loading="loading" :data="orphans" :columns="[
|
||||||
|
{ title: '表单名', key: 'name', tooltip: true },
|
||||||
|
{ title: 'FC code', key: 'code', width: 160 },
|
||||||
|
{ title: 'uuid', key: 'formUuid', width: 290, tooltip: true },
|
||||||
|
{ title: 'FC 状态', key: 'statusName', width: 130, slot: 'status' },
|
||||||
|
{ title: '受众', key: 'audience', width: 160, slot: 'audience' },
|
||||||
|
{ title: '更新时间', key: 'updateTime', width: 170 },
|
||||||
|
{ title: '操作', slot: 'action', width: 180 },
|
||||||
|
]">
|
||||||
|
<template #status="{ row }">
|
||||||
|
{{ statusText(row) }}
|
||||||
|
</template>
|
||||||
|
<template #audience="{ row }">
|
||||||
|
{{ (row.audience || []).join('/') || '—' }}
|
||||||
|
</template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<Button size="small" type="primary" ghost @click="register(row)">补登记</Button>
|
||||||
|
<Button size="small" type="error" ghost style="margin-left: 6px" @click="clean(row)">清理</Button>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<div v-if="!loading && !orphans.length" class="empty">无未挂接表单,绑定关系一致。</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { Message, Modal } from "view-ui-plus";
|
||||||
|
import QRCode from "qrcode";
|
||||||
|
import config from "@/assets/config/host.config";
|
||||||
|
import { questionApi, QuestionnaireVo } from "@/api/question";
|
||||||
|
import { fcApi } from "@/api/fc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布投放:先(可选)调整 FC 配置(受众/截止/限次,直调 FC config)→
|
||||||
|
* 问卷侧 publish(后端 fetchSchema 核验 FC 已发布→钉版+status=1)→
|
||||||
|
* 生成填写链接 {FC前端}/#/f/{code}(匿名问卷同链接,FC 端自动探测 /anon/form/*)+ 二维码(前端生成)。
|
||||||
|
*/
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const id = computed(() => Number(route.query.id || 0));
|
||||||
|
|
||||||
|
const q = ref<QuestionnaireVo | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const publishing = ref(false);
|
||||||
|
const finishing = ref(false);
|
||||||
|
const qrDataUrl = ref("");
|
||||||
|
|
||||||
|
const conf = ref({ audience: [] as string[], fillLimit: 0, deadline: "" });
|
||||||
|
|
||||||
|
const fillLink = computed(() =>
|
||||||
|
q.value?.formCode ? `${config.fcFrontHost}/#/f/${q.value.formCode}` : "");
|
||||||
|
|
||||||
|
const anon = computed(() => conf.value.audience.includes("ANONYMOUS"));
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
q.value = await questionApi.detail(id.value).send();
|
||||||
|
const fcForm = await fcApi.get(q.value.formUuid).send();
|
||||||
|
conf.value.audience = fcForm.audience?.length ? [...fcForm.audience] : ["STUDENT"];
|
||||||
|
conf.value.fillLimit = fcForm.fillLimit || 0;
|
||||||
|
conf.value.deadline = fcForm.deadline ? fcForm.deadline.split(" ")[0] : "";
|
||||||
|
if (q.value.status === 1 && fillLink.value) {
|
||||||
|
qrDataUrl.value = await QRCode.toDataURL(fillLink.value, { width: 220, margin: 1 });
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "加载失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 保存 FC 配置(受众/截止/限次;frozen 建卷时已锁 1) */
|
||||||
|
const saveConfig = async () => {
|
||||||
|
if (!conf.value.audience.length) {
|
||||||
|
Message.warning("请选择面向人群");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await fcApi.config({
|
||||||
|
uuid: q.value!.formUuid,
|
||||||
|
audience: conf.value.audience,
|
||||||
|
fillLimit: anon.value ? 0 : conf.value.fillLimit,
|
||||||
|
deadline: conf.value.deadline || "",
|
||||||
|
}).send();
|
||||||
|
Message.success("FC 配置已保存");
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "保存失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 发布:问卷侧核验(FC 已发布)→ 钉版 status=1 → 出链接/二维码 */
|
||||||
|
const publish = async () => {
|
||||||
|
publishing.value = true;
|
||||||
|
try {
|
||||||
|
q.value = await questionApi.publish(id.value).send();
|
||||||
|
qrDataUrl.value = await QRCode.toDataURL(fillLink.value, { width: 220, margin: 1 });
|
||||||
|
Message.success("发布成功,问卷进入收集中");
|
||||||
|
} catch (e: any) {
|
||||||
|
// 常见拒绝:FC 侧尚未发布(需先在设计器里点发布)
|
||||||
|
Message.error(e?.message || "发布失败");
|
||||||
|
} finally {
|
||||||
|
publishing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyLink = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(fillLink.value);
|
||||||
|
Message.success("链接已复制");
|
||||||
|
} catch {
|
||||||
|
Message.warning("复制失败,请手动选择复制");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 结束投放:问卷 status=2;可选同步 FC 停用(硬门禁——FC 直链也会被断) */
|
||||||
|
const finish = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "结束投放",
|
||||||
|
content: "结束后问卷侧入口隐藏并提示已结束。是否同时停用 FC 表单?(停用是硬门禁:已投出的直链也无法再提交;数据仍可读可导)",
|
||||||
|
okText: "结束并停用 FC",
|
||||||
|
cancelText: "仅结束问卷",
|
||||||
|
onOk: async () => {
|
||||||
|
finishing.value = true;
|
||||||
|
try {
|
||||||
|
await questionApi.finish(id.value).send();
|
||||||
|
await fcApi.disable(q.value!.formUuid).send();
|
||||||
|
Message.success("已结束投放并停用 FC 表单");
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "操作失败");
|
||||||
|
} finally {
|
||||||
|
finishing.value = false;
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: async () => {
|
||||||
|
finishing.value = true;
|
||||||
|
try {
|
||||||
|
await questionApi.finish(id.value).send();
|
||||||
|
Message.success("已结束投放");
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "操作失败");
|
||||||
|
} finally {
|
||||||
|
finishing.value = false;
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const reopen = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
q.value = await questionApi.reopen(id.value).send();
|
||||||
|
Message.success("已恢复收集");
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "操作失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(load);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="bar">
|
||||||
|
<Button @click="router.push('/')">← 返回列表</Button>
|
||||||
|
<span class="title">发布投放{{ q ? `:${q.titleSnapshot}` : "" }}</span>
|
||||||
|
<Tag v-if="q?.status === 1" color="success">收集中</Tag>
|
||||||
|
<Tag v-else-if="q?.status === 2" color="warning">已结束</Tag>
|
||||||
|
<Tag v-else color="default">草稿</Tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<Card dis-hover title="投放配置(写入 FC 表单)">
|
||||||
|
<Form :label-width="110">
|
||||||
|
<FormItem label="面向人群">
|
||||||
|
<CheckboxGroup v-model="conf.audience">
|
||||||
|
<Checkbox label="STUDENT">学生</Checkbox>
|
||||||
|
<Checkbox label="TEACHER">教师</Checkbox>
|
||||||
|
<Checkbox label="ANONYMOUS">匿名(无需登录)</Checkbox>
|
||||||
|
</CheckboxGroup>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="限填次数">
|
||||||
|
<RadioGroup v-model="conf.fillLimit">
|
||||||
|
<Radio :label="0" :disabled="anon">不限</Radio>
|
||||||
|
<Radio :label="1" :disabled="anon">限一次(实名)</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="截止日期">
|
||||||
|
<DatePicker v-model="conf.deadline" type="date" format="yyyy-MM-dd"
|
||||||
|
placeholder="当日 23:59:59 截止" style="width: 200px" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem>
|
||||||
|
<Button :loading="loading" @click="saveConfig">保存配置到 FC</Button>
|
||||||
|
<Poptip v-if="q?.status !== 1" trigger="hover" title="发布前请确认"
|
||||||
|
content="FC 设计器里需已保存并点击发布;此处发布会核验 FC 发布状态。"
|
||||||
|
style="margin-left: 8px">
|
||||||
|
<Button type="primary" :loading="publishing" @click="publish">发布问卷</Button>
|
||||||
|
</Poptip>
|
||||||
|
<Button v-if="q?.status === 1" type="error" ghost style="margin-left: 8px" :loading="finishing"
|
||||||
|
@click="finish">结束投放</Button>
|
||||||
|
<Button v-if="q?.status === 2" type="primary" ghost style="margin-left: 8px" :loading="loading"
|
||||||
|
@click="reopen">恢复收集</Button>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card dis-hover title="填写链接与二维码">
|
||||||
|
<div v-if="q?.status === 1 || q?.status === 2" class="link-box">
|
||||||
|
<div class="link-row">
|
||||||
|
<Input readonly :model-value="fillLink" />
|
||||||
|
<Button type="primary" ghost style="margin-left: 8px" @click="copyLink">复制</Button>
|
||||||
|
</div>
|
||||||
|
<p class="hint">匿名问卷同链接:FC 填写页自动探测未登录并切匿名通道。</p>
|
||||||
|
<div v-if="qrDataUrl" class="qr">
|
||||||
|
<img :src="qrDataUrl" alt="问卷二维码" />
|
||||||
|
<p>手机扫码填写</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty">
|
||||||
|
<p>尚未发布。保存配置并点击「发布问卷」后,此处生成填写链接与二维码。</p>
|
||||||
|
<p class="hint">发布顺序:FC 设计器内保存题目 → 设计器内点发布 → 此处「发布问卷」核验钉版。</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.link-box .link-row {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.qr {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.qr img {
|
||||||
|
width: 220px;
|
||||||
|
height: 220px;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
color: #666;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { Message } from "view-ui-plus";
|
||||||
|
import { questionApi, StatsVo, QuestionnaireVo } from "@/api/question";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础统计(全部在答卷投影上算):
|
||||||
|
* - 回收量/状态分布;
|
||||||
|
* - 选项题按题分布——人数口径(一份答卷对一题的一个选项最多计 1;与 FC"选次"口径可区分,页面上注明);
|
||||||
|
* - 文本题明细列表。
|
||||||
|
*/
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const id = computed(() => Number(route.query.id || 0));
|
||||||
|
|
||||||
|
const q = ref<QuestionnaireVo | null>(null);
|
||||||
|
const stats = ref<StatsVo | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
q.value = await questionApi.detail(id.value).send();
|
||||||
|
stats.value = await questionApi.stats(id.value).send();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "加载失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(load);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="bar">
|
||||||
|
<Button @click="router.push('/')">← 返回列表</Button>
|
||||||
|
<span class="title">基础统计{{ q ? `:${q.titleSnapshot}` : "" }}</span>
|
||||||
|
<Tag v-if="q?.formVersion" color="blue">钉版 v{{ q.formVersion }}</Tag>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<Button :loading="loading" @click="load">刷新</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cards" v-if="stats">
|
||||||
|
<Card dis-hover class="kpi">
|
||||||
|
<div class="kpi-num">{{ stats.total }}</div>
|
||||||
|
<div class="kpi-label">回收量(份,不含已删)</div>
|
||||||
|
</Card>
|
||||||
|
<Card dis-hover>
|
||||||
|
<template #title>状态分布</template>
|
||||||
|
<div v-for="s in stats.byStatus" :key="s.status" class="status-row">
|
||||||
|
<span>{{ s.status || "未设置" }}</span>
|
||||||
|
<span class="num">{{ s.count }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="!stats.byStatus.length" class="hint">暂无数据</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card v-if="stats?.byField?.length" dis-hover title="选项题分布(人数口径)" style="margin-top: 16px">
|
||||||
|
<Alert show-icon style="margin-bottom: 12px">
|
||||||
|
人数口径
|
||||||
|
<template #desc>一份答卷对一题的一个选项最多计 1;多选按答卷去重后逐选项计数。与 FC 侧 answer_index 的"选次"口径不同(一人选两项在两桶各计 1 的选次口径不可区分答卷)。</template>
|
||||||
|
</Alert>
|
||||||
|
<div v-for="f in stats.byField" :key="f.key" class="field-block">
|
||||||
|
<div class="field-title">
|
||||||
|
{{ f.label }}
|
||||||
|
<span class="hint">({{ f.answeredSheets }} 份答卷作答本题)</span>
|
||||||
|
</div>
|
||||||
|
<div v-for="item in f.items" :key="item.value" class="bar-row">
|
||||||
|
<span class="bar-label">{{ item.label }}</span>
|
||||||
|
<div class="bar-track">
|
||||||
|
<div class="bar-fill" :style="{ width: Math.min(item.percent, 100) + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<span class="bar-num">{{ item.count }} 人({{ item.percent }}%)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card v-if="stats?.textDetail?.length" dis-hover title="文本/数值题明细" style="margin-top: 16px">
|
||||||
|
<div v-for="t in stats.textDetail" :key="t.key" class="field-block">
|
||||||
|
<div class="field-title">{{ t.label }} <span class="hint">({{ t.type }},{{ t.items.length }} 条)</span></div>
|
||||||
|
<ul class="text-list">
|
||||||
|
<li v-for="(item, i) in t.items" :key="i">{{ item }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div v-if="stats && !stats.byField.length && !stats.textDetail.length" class="empty">
|
||||||
|
暂无统计数据(问卷发布后,答卷投影 30 秒内自动同步;也可在回收明细页点「立即拉取」)。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 260px 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.kpi {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.kpi-num {
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2d8cf0;
|
||||||
|
}
|
||||||
|
.kpi-label {
|
||||||
|
color: #999;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.status-row .num {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.field-block {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.field-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.bar-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.bar-label {
|
||||||
|
width: 140px;
|
||||||
|
text-align: right;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.bar-track {
|
||||||
|
flex: 1;
|
||||||
|
height: 16px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #2d8cf0, #5cadff);
|
||||||
|
border-radius: 8px;
|
||||||
|
min-width: 2px;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
.bar-num {
|
||||||
|
width: 160px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.text-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.text-list li {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
color: #999;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { Message } from "view-ui-plus";
|
||||||
|
import { questionApi } from "@/api/question";
|
||||||
|
import { fcApi } from "@/api/fc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建向导——幂等三步(任务书 §4.1):
|
||||||
|
* ① FC create(appCode=question)② FC config(frozen=1+受众/限次/截止)③ 问卷侧登记(uk form_uuid 兜底)。
|
||||||
|
* 第三步失败:提示仅重试第三步(不自动回滚 FC 表单——孤儿由"未挂接表单"对账页只读展示)。
|
||||||
|
*/
|
||||||
|
const router = useRouter();
|
||||||
|
const step = ref(0); // 0 表单填写 1 执行中 2 完成
|
||||||
|
const submitting = ref(false);
|
||||||
|
const retryThird = ref(false); // 第三步失败后的重试形态(隐藏①②)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
audience: ["STUDENT"] as string[],
|
||||||
|
anonymous: computed(() => form.audience.includes("ANONYMOUS")),
|
||||||
|
fillLimit: 0 as number, // 0 不限 / 1 限一次
|
||||||
|
deadline: "" as string, // yyyy-MM-dd(FC 端补 23:59:59)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 匿名+限次互斥(FC updateConfig 硬拦,前端先行提示)
|
||||||
|
const fillLimitDisabled = computed(() => form.anonymous);
|
||||||
|
watchAnonymousLimit();
|
||||||
|
|
||||||
|
function watchAnonymousLimit() {
|
||||||
|
if (form.anonymous && form.fillLimit === 1) {
|
||||||
|
form.fillLimit = 0;
|
||||||
|
Message.warning("匿名问卷与限填一次互斥(FC 硬性约束),已重置为不限次数");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = reactive({ formUuid: "", formCode: "", questionnaireId: 0 as number });
|
||||||
|
|
||||||
|
const step1Create = async () => {
|
||||||
|
const created1 = await fcApi.create(form.title).send();
|
||||||
|
created.formUuid = created1.formUuid;
|
||||||
|
created.formCode = created1.code;
|
||||||
|
step.value = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const step2Config = async () => {
|
||||||
|
await fcApi.config({
|
||||||
|
uuid: created.formUuid,
|
||||||
|
audience: form.audience,
|
||||||
|
fillLimit: form.anonymous ? 0 : form.fillLimit,
|
||||||
|
deadline: form.deadline || undefined,
|
||||||
|
frozen: 1, // 问卷答卷一律冻结(append-only 游标拉取的前提)
|
||||||
|
anonCookieMinutes: form.anonymous ? 60 : undefined,
|
||||||
|
}).send();
|
||||||
|
step.value = 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
const step3Register = async () => {
|
||||||
|
const q = await questionApi.register({
|
||||||
|
formUuid: created.formUuid,
|
||||||
|
formCode: created.formCode,
|
||||||
|
title: form.title,
|
||||||
|
anonymous: form.anonymous,
|
||||||
|
description: JSON.stringify({ description: form.description || "" }),
|
||||||
|
}).send();
|
||||||
|
created.questionnaireId = q.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!form.title.trim()) {
|
||||||
|
Message.warning("请填写问卷标题");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!form.audience.length) {
|
||||||
|
Message.warning("请选择面向人群");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
if (!retryThird.value) {
|
||||||
|
await step1Create();
|
||||||
|
await step2Config();
|
||||||
|
}
|
||||||
|
await step3Register();
|
||||||
|
Message.success(`问卷「${form.title}」创建成功,接下来在编辑器里添加题目`);
|
||||||
|
router.push({ path: "/edit", query: { id: created.questionnaireId } });
|
||||||
|
} catch (e: any) {
|
||||||
|
const msg = e?.message || "创建失败";
|
||||||
|
if (created.formUuid) {
|
||||||
|
// ①② 已成功——只提示重试第三步(幂等:重复登记不产生重复行)
|
||||||
|
retryThird.value = true;
|
||||||
|
Message.error(`FC 表单已创建,但问卷登记失败(${msg})。请点击"重试登记",不要重复新建。`);
|
||||||
|
} else {
|
||||||
|
Message.error(`创建失败:${msg}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<Card dis-hover>
|
||||||
|
<template #title>新建问卷(三步:创建 FC 表单 → 投放配置 → 登记绑定)</template>
|
||||||
|
<Steps :current="retryThird ? 2 : 0" style="margin-bottom: 20px">
|
||||||
|
<Step title="填写信息" content="标题/人群/截止/限次"></Step>
|
||||||
|
<Step title="FC 建表+配置" content="自动执行"></Step>
|
||||||
|
<Step title="登记绑定" content="问卷侧行落库"></Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Form :label-width="110" style="max-width: 620px">
|
||||||
|
<FormItem label="问卷标题" required>
|
||||||
|
<Input v-model="form.title" :disabled="retryThird" placeholder="如:食堂满意度调查" maxlength="60" show-word-limit />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="问卷说明">
|
||||||
|
<Input v-model="form.description" type="textarea" :disabled="retryThird" :rows="3"
|
||||||
|
placeholder="将展示在填写页顶部(选填)" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="面向人群" required>
|
||||||
|
<CheckboxGroup v-model="form.audience">
|
||||||
|
<Checkbox label="STUDENT" :disabled="retryThird">学生</Checkbox>
|
||||||
|
<Checkbox label="TEACHER" :disabled="retryThird">教师</Checkbox>
|
||||||
|
<Checkbox label="ANONYMOUS" :disabled="retryThird">匿名(无需登录)</Checkbox>
|
||||||
|
</CheckboxGroup>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="限填次数">
|
||||||
|
<RadioGroup v-model="form.fillLimit">
|
||||||
|
<Radio :label="0" :disabled="fillLimitDisabled || retryThird">不限</Radio>
|
||||||
|
<Radio :label="1" :disabled="fillLimitDisabled || retryThird">限一次(实名)</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
<span v-if="fillLimitDisabled" class="hint">匿名问卷不可限次(FC 硬性互斥)</span>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="截止日期">
|
||||||
|
<DatePicker v-model="form.deadline" type="date" format="yyyy-MM-dd" placeholder="选填,当日 23:59:59 截止"
|
||||||
|
style="width: 220px" :disabled="retryThird" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem>
|
||||||
|
<Button type="primary" :loading="submitting" @click="submit">
|
||||||
|
{{ retryThird ? "重试登记(仅第三步)" : "创建问卷" }}
|
||||||
|
</Button>
|
||||||
|
<Button style="margin-left: 8px" @click="router.push('/')">取消</Button>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue","types"],
|
||||||
|
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
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 ViteAutoImport from "unplugin-auto-import/vite";
|
||||||
|
|
||||||
|
import UnoCSS from "@unocss/vite";
|
||||||
|
import presetUno from "@unocss/preset-uno";
|
||||||
|
|
||||||
|
import presetRemToPx from "@unocss/preset-rem-to-px";
|
||||||
|
|
||||||
|
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
||||||
|
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__",
|
||||||
|
}),
|
||||||
|
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"],
|
||||||
|
}),
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
fontSize: {
|
||||||
|
xs: "12px",
|
||||||
|
sm: "14px",
|
||||||
|
base: "16px",
|
||||||
|
lg: "18px",
|
||||||
|
xl: "20px",
|
||||||
|
"2xl": "24px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: "localhost",
|
||||||
|
port: 8091,
|
||||||
|
proxy: {
|
||||||
|
// 问卷后端(question-center :8090):同源代理规避 CAS 过滤器不回 CORS 头
|
||||||
|
"/api": {
|
||||||
|
target: "http://localhost:8090",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
// FC 后端(form-center :8086):前端以登录会话直调 FC /api/form/**(create/config/get/
|
||||||
|
// fillable/copy 等),rewrite 去掉 /fcapi 前缀;credentials:"include"(localhost 各端口
|
||||||
|
// 共享 Cookie 域,会话互通)
|
||||||
|
"/fcapi": {
|
||||||
|
target: "http://localhost:8086",
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (p: string) => p.replace(/^\/fcapi/, ""),
|
||||||
|
},
|
||||||
|
// 主题通道本地联调(生产同源;YH_THEME_PROXY_TARGET 覆盖本地通道源)
|
||||||
|
"/static": {
|
||||||
|
target: process.env.YH_THEME_PROXY_TARGET || "http://base.yunhaixingtong.com",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
css: {},
|
||||||
|
base: "./",
|
||||||
|
build: {
|
||||||
|
target: "modules",
|
||||||
|
outDir,
|
||||||
|
assetsDir: "assets",
|
||||||
|
sourcemap: false,
|
||||||
|
minify: "esbuild",
|
||||||
|
chunkSizeWarningLimit: 1000,
|
||||||
|
cssTarget: "chrome61",
|
||||||
|
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]";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||