mirror of
https://github.com/certd/certd.git
synced 2026-04-24 04:17:25 +08:00
build: trident-sync prepare
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<a-config-provider :locale="locale">
|
||||
<router-view v-if="routerEnabled" />
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import zhCN from "ant-design-vue/es/locale/zh_CN";
|
||||
import enUS from "ant-design-vue/es/locale/en_US";
|
||||
import { provide, ref, nextTick } from "vue";
|
||||
import { usePageStore } from "/src/store/modules/page";
|
||||
import { useResourceStore } from "/src/store/modules/resource";
|
||||
import { useSettingStore } from "/@/store/modules/settings";
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import 'dayjs/locale/en';
|
||||
import dayjs from 'dayjs'
|
||||
export default {
|
||||
name: "App",
|
||||
setup() {
|
||||
//刷新页面方法
|
||||
const routerEnabled = ref(true);
|
||||
const locale = ref(zhCN);
|
||||
async function reload() {
|
||||
routerEnabled.value = false;
|
||||
await nextTick();
|
||||
routerEnabled.value = true;
|
||||
}
|
||||
function localeChanged(value) {
|
||||
console.log("locale changed:", value);
|
||||
if (value === "zh-cn") {
|
||||
locale.value = zhCN;
|
||||
dayjs.locale('zh-cn');
|
||||
} else if (value === "en") {
|
||||
locale.value = enUS;
|
||||
dayjs.locale('en');
|
||||
}
|
||||
}
|
||||
localeChanged('zh-cn')
|
||||
provide("fn:router.reload", reload);
|
||||
provide("fn:locale.changed", localeChanged);
|
||||
|
||||
//其他初始化
|
||||
const resourceStore = useResourceStore();
|
||||
resourceStore.init();
|
||||
const pageStore = usePageStore();
|
||||
pageStore.init();
|
||||
const settingStore = useSettingStore();
|
||||
settingStore.init();
|
||||
|
||||
return {
|
||||
routerEnabled,
|
||||
locale
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
export default [
|
||||
{
|
||||
path: "/login",
|
||||
method: "post",
|
||||
handle() {
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: {
|
||||
token: "faker token",
|
||||
expire: 10000
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/sys/authority/user/mine",
|
||||
method: "get",
|
||||
handle() {
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: {
|
||||
id: 1,
|
||||
username: "username",
|
||||
nickName: "admin"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,51 @@
|
||||
import { request, requestForMock } from "../service";
|
||||
import { env } from "/@/utils/util.env";
|
||||
/**
|
||||
* @description: Login interface parameters
|
||||
*/
|
||||
export interface LoginReq {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface UserInfoRes {
|
||||
id: string | number;
|
||||
username: string;
|
||||
nickName: string;
|
||||
}
|
||||
|
||||
export interface LoginRes {
|
||||
token: string;
|
||||
expire: number;
|
||||
}
|
||||
|
||||
export async function login(data: LoginReq): Promise<LoginRes> {
|
||||
if (env.PM_ENABLED === "false") {
|
||||
//没有开启权限模块,模拟登录
|
||||
return await requestForMock({
|
||||
url: "/login",
|
||||
method: "post",
|
||||
data
|
||||
});
|
||||
}
|
||||
//如果开启了登录与权限模块,则真实登录
|
||||
return await request({
|
||||
url: "/login",
|
||||
method: "post",
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export async function mine(): Promise<UserInfoRes> {
|
||||
if (env.PM_ENABLED === "false") {
|
||||
//没有开启权限模块,模拟登录
|
||||
return await requestForMock({
|
||||
url: "/sys/authority/user/mine",
|
||||
method: "post"
|
||||
});
|
||||
}
|
||||
return await request({
|
||||
url: "/sys/authority/user/mine",
|
||||
method: "post"
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import axios from "axios";
|
||||
import { get } from "lodash-es";
|
||||
import Adapter from "axios-mock-adapter";
|
||||
import { errorLog, errorCreate } from "./tools";
|
||||
import { env } from "/src/utils/util.env";
|
||||
import { useUserStore } from "../store/modules/user";
|
||||
/**
|
||||
* @description 创建请求实例
|
||||
*/
|
||||
function createService() {
|
||||
// 创建一个 axios 实例
|
||||
const service = axios.create();
|
||||
// 请求拦截
|
||||
service.interceptors.request.use(
|
||||
(config) => config,
|
||||
(error) => {
|
||||
// 发送失败
|
||||
console.log(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// 响应拦截
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
if (response.config.responseType === "blob") {
|
||||
return response;
|
||||
}
|
||||
// dataAxios 是 axios 返回数据中的 data
|
||||
const dataAxios = response.data;
|
||||
// 这个状态码是和后端约定的
|
||||
const { code } = dataAxios;
|
||||
// 根据 code 进行判断
|
||||
if (code === undefined) {
|
||||
// 如果没有 code 代表这不是项目后端开发的接口 比如可能是 D2Admin 请求最新版本
|
||||
errorCreate(`非标准返回:${dataAxios}, ${response.config.url}`);
|
||||
return dataAxios;
|
||||
} else {
|
||||
// 有 code 代表这是一个后端接口 可以进行进一步的判断
|
||||
switch (code) {
|
||||
case 0:
|
||||
// [ 示例 ] code === 0 代表没有错误
|
||||
// @ts-ignore
|
||||
if (response.config.unpack === false) {
|
||||
//如果不需要解包
|
||||
return dataAxios;
|
||||
}
|
||||
return dataAxios.data;
|
||||
default:
|
||||
// 不是正确的 code
|
||||
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
|
||||
return dataAxios;
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
const status = get(error, "response.status");
|
||||
switch (status) {
|
||||
case 400:
|
||||
error.message = "请求错误";
|
||||
break;
|
||||
case 401:
|
||||
error.message = "未授权,请登录";
|
||||
break;
|
||||
case 403:
|
||||
error.message = "拒绝访问";
|
||||
break;
|
||||
case 404:
|
||||
error.message = `请求地址出错: ${error.response.config.url}`;
|
||||
break;
|
||||
case 408:
|
||||
error.message = "请求超时";
|
||||
break;
|
||||
case 500:
|
||||
error.message = "服务器内部错误";
|
||||
break;
|
||||
case 501:
|
||||
error.message = "服务未实现";
|
||||
break;
|
||||
case 502:
|
||||
error.message = "网关错误";
|
||||
break;
|
||||
case 503:
|
||||
error.message = "服务不可用";
|
||||
break;
|
||||
case 504:
|
||||
error.message = "网关超时";
|
||||
break;
|
||||
case 505:
|
||||
error.message = "HTTP版本不受支持";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
errorLog(error);
|
||||
if (status === 401) {
|
||||
const userStore = useUserStore();
|
||||
userStore.logout();
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
return service;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 创建请求方法
|
||||
* @param {Object} service axios 实例
|
||||
*/
|
||||
function createRequestFunction(service) {
|
||||
return function (config) {
|
||||
const configDefault = {
|
||||
headers: {
|
||||
"Content-Type": get(config, "headers.Content-Type", "application/json")
|
||||
},
|
||||
timeout: 5000,
|
||||
baseURL: env.API,
|
||||
data: {}
|
||||
};
|
||||
const userStore = useUserStore();
|
||||
const token = userStore.getToken;
|
||||
if (token != null) {
|
||||
// @ts-ignore
|
||||
configDefault.headers.Authorization = token;
|
||||
}
|
||||
return service(Object.assign(configDefault, config));
|
||||
};
|
||||
}
|
||||
|
||||
// 用于真实网络请求的实例和请求方法
|
||||
export const service = createService();
|
||||
export const request = createRequestFunction(service);
|
||||
|
||||
// 用于模拟网络请求的实例和请求方法
|
||||
export const serviceForMock = createService();
|
||||
export const requestForMock = createRequestFunction(serviceForMock);
|
||||
|
||||
// 网络请求数据模拟工具
|
||||
export const mock = new Adapter(serviceForMock, { delayResponse: 200 });
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @description 安全地解析 json 字符串
|
||||
* @param {String} jsonString 需要解析的 json 字符串
|
||||
* @param {String} defaultValue 默认值
|
||||
*/
|
||||
import { uiContext } from "@fast-crud/fast-crud";
|
||||
|
||||
export function parse(jsonString = "{}", defaultValue = {}) {
|
||||
let result = defaultValue;
|
||||
try {
|
||||
result = JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 接口请求返回
|
||||
* @param {Any} data 返回值
|
||||
* @param {String} msg 状态信息
|
||||
* @param {Number} code 状态码
|
||||
*/
|
||||
export function response(data = {}, msg = "", code = 0) {
|
||||
return [200, { code, msg, data }];
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 接口请求返回 正确返回
|
||||
* @param {Any} data 返回值
|
||||
* @param {String} msg 状态信息
|
||||
*/
|
||||
export function responseSuccess(data = {}, msg = "成功") {
|
||||
return response(data, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 接口请求返回 错误返回
|
||||
* @param {Any} data 返回值
|
||||
* @param {String} msg 状态信息
|
||||
* @param {Number} code 状态码
|
||||
*/
|
||||
export function responseError(data = {}, msg = "请求失败", code = 500) {
|
||||
return response(data, msg, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 记录和显示错误
|
||||
* @param {Error} error 错误对象
|
||||
*/
|
||||
export function errorLog(error) {
|
||||
// 打印到控制台
|
||||
console.error(error);
|
||||
// 显示提示
|
||||
uiContext.get().notification.error({ message: error.message });
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 创建一个错误
|
||||
* @param {String} msg 错误信息
|
||||
*/
|
||||
export function errorCreate(msg) {
|
||||
const error = new Error(msg);
|
||||
errorLog(error);
|
||||
throw error;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group 21</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
|
||||
<g id="Group-21" transform="translate(77.000000, 73.000000)">
|
||||
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
|
||||
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
|
||||
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
|
||||
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
|
||||
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
|
||||
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
|
||||
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
|
||||
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
|
||||
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
|
||||
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
|
||||
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
|
||||
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
|
||||
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
|
||||
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
|
||||
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
|
||||
</g>
|
||||
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
|
||||
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
|
||||
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
|
||||
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
|
||||
</g>
|
||||
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
|
||||
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
|
||||
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
|
||||
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
|
||||
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
|
||||
</g>
|
||||
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
|
||||
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
|
||||
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
|
||||
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
|
||||
</g>
|
||||
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
|
||||
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
|
||||
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
|
||||
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
|
||||
</g>
|
||||
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
|
||||
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
|
||||
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
|
||||
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
|
||||
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
|
||||
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
|
||||
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
|
||||
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
|
||||
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="pi-container">
|
||||
<div class="box">
|
||||
<div class="inner">
|
||||
<div class="header">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div class="body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "PiContainer"
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.pi-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.box {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
.inner {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.body {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
.footer {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { request } from "/src/api/service";
|
||||
|
||||
const apiPrefix = "/pi/dnsProvider";
|
||||
|
||||
export async function GetList() {
|
||||
return await request({
|
||||
url: apiPrefix + "/list",
|
||||
method: "post"
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<a-select class="pi-dns-provider-selector" :value="modelValue" :options="options" @update:value="onChanged">
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { inject, Ref, ref, watch } from "vue";
|
||||
import * as api from "./api";
|
||||
|
||||
export default {
|
||||
name: "PiDnsProviderSelector",
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, ctx) {
|
||||
const options = ref<any[]>([]);
|
||||
|
||||
async function onCreate() {
|
||||
const list = await api.GetList();
|
||||
const array: any[] = [];
|
||||
for (let item of list) {
|
||||
array.push({
|
||||
value: item.name,
|
||||
label: item.title
|
||||
});
|
||||
}
|
||||
options.value = array;
|
||||
if (props.modelValue == null && options.value.length > 0) {
|
||||
ctx.emit("update:modelValue", options.value[0].value);
|
||||
}
|
||||
}
|
||||
onCreate();
|
||||
|
||||
function onChanged(value) {
|
||||
ctx.emit("update:modelValue", value);
|
||||
}
|
||||
return {
|
||||
options,
|
||||
onChanged
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.step-edit-form {
|
||||
.body {
|
||||
padding: 10px;
|
||||
.ant-card {
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.current {
|
||||
border-color: #00b7ff;
|
||||
}
|
||||
|
||||
.ant-card-meta-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.ant-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-left: 5px;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 14px;
|
||||
height: 100px;
|
||||
|
||||
overflow-y: hidden;
|
||||
|
||||
.ant-card-meta-description {
|
||||
font-size: 10px;
|
||||
line-height: 20px;
|
||||
height: 40px;
|
||||
color: #7f7f7f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="pi-editable" :class="{ disabled, 'hover-show': hoverShow }">
|
||||
<div v-if="isEdit" class="input">
|
||||
<a-input
|
||||
ref="inputRef"
|
||||
v-model:value="valueRef"
|
||||
:validate-status="modelValue ? '' : 'error'"
|
||||
v-bind="input"
|
||||
@keyup.enter="save()"
|
||||
@blur="save()"
|
||||
>
|
||||
<template #suffix>
|
||||
<fs-icon icon="ant-design:check-outlined" @click="save()"></fs-icon>
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
<div v-else class="view" @click="edit">
|
||||
<span> {{ modelValue }}</span>
|
||||
<fs-icon class="edit-icon" icon="ant-design:edit-outlined"></fs-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { watch, ref, nextTick } from "vue";
|
||||
|
||||
export default {
|
||||
name: "PiEditable",
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
input: {
|
||||
type: Object
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
hoverShow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, ctx) {
|
||||
const inputRef = ref();
|
||||
const valueRef = ref(props.modelValue);
|
||||
watch(
|
||||
() => {
|
||||
return props.modelValue;
|
||||
},
|
||||
(value) => {
|
||||
valueRef.value = value;
|
||||
}
|
||||
);
|
||||
const isEdit = ref(false);
|
||||
async function edit() {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
isEdit.value = true;
|
||||
await nextTick();
|
||||
inputRef.value.focus();
|
||||
}
|
||||
function save() {
|
||||
isEdit.value = false;
|
||||
ctx.emit("update:modelValue", valueRef.value);
|
||||
}
|
||||
return {
|
||||
valueRef,
|
||||
isEdit,
|
||||
save,
|
||||
edit,
|
||||
inputRef
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.pi-editable {
|
||||
line-height: 34px;
|
||||
|
||||
span.fs-iconify {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.edit-icon {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
&.hover-show {
|
||||
.edit-icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
&:hover {
|
||||
.edit-icon {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
.edit-icon {
|
||||
line-height: 34px;
|
||||
}
|
||||
.view {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
import PiContainer from "./container.vue";
|
||||
import PiAccessSelector from "../views/certd/access/access-selector/index.vue";
|
||||
import PiDnsProviderSelector from "./dns-provider-selector/index.vue";
|
||||
import PiOutputSelector from "../views/certd/pipeline/pipeline/component/output-selector/index.vue";import PiEditable from "./editable.vue";
|
||||
import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue";
|
||||
export default {
|
||||
install(app) {
|
||||
app.component("PiContainer", PiContainer);
|
||||
app.component("PiAccessSelector", PiAccessSelector);
|
||||
app.component("PiEditable", PiEditable);
|
||||
app.component("PiOutputSelector", PiOutputSelector);
|
||||
app.component("PiDnsProviderSelector", PiDnsProviderSelector);
|
||||
|
||||
app.component("CheckCircleOutlined", CheckCircleOutlined);
|
||||
app.component("InfoCircleOutlined", InfoCircleOutlined);
|
||||
app.component("UndoOutlined", UndoOutlined);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
// @ts-ignore
|
||||
import { createI18n } from "vue-i18n";
|
||||
//
|
||||
// @ts-ignore
|
||||
import enFsLocale from "@fast-crud/fast-crud/dist/locale/lang/en.js";
|
||||
// @ts-ignore
|
||||
import zhFsLocale from "@fast-crud/fast-crud/dist/locale/lang/zh-cn.js";
|
||||
import en from "./locale/en";
|
||||
import zh from "./locale/zh_CN";
|
||||
const messages = {
|
||||
en: {
|
||||
label: "English",
|
||||
// 定义您自己的字典,但是请不要和 `fs` 重复,这样会导致 fast-crud 内部组件的翻译失效.
|
||||
fs: enFsLocale.fs,
|
||||
...en
|
||||
},
|
||||
"zh-cn": {
|
||||
label: "简体中文",
|
||||
// 定义您自己的字典,但是请不要和 `fs` 重复,这样会导致 fast-crud 内部组件的翻译失效.
|
||||
fs: zhFsLocale.fs,
|
||||
...zh
|
||||
}
|
||||
};
|
||||
|
||||
export default createI18n({
|
||||
legacy: false,
|
||||
locale: "zh-cn",
|
||||
fallbackLocale: "zh-cn",
|
||||
messages
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
app: { crud: { i18n: { name: "name", city: "city", status: "status" } } }
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
app: {
|
||||
crud: { i18n: { name: "姓名", city: "城市", status: "状态" } },
|
||||
login: {
|
||||
logoutTip: "确认",
|
||||
logoutMessage: "确定要注销登录吗?"
|
||||
}
|
||||
}
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="fs-contentmenu-list" @click="rowClick">
|
||||
<div
|
||||
v-for="item in menulist"
|
||||
:key="item.value"
|
||||
:data-value="item.value"
|
||||
class="fs-contentmenu-item"
|
||||
flex="cross:center main:center"
|
||||
>
|
||||
<d2-icon v-if="item.icon" :name="item.icon" />
|
||||
<div class="fs-contentmenu-item-title" flex-box="1">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "FsContextmenuList",
|
||||
props: {
|
||||
menulist: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rowClick(event) {
|
||||
let target = event.target;
|
||||
while (!target.dataset.value) {
|
||||
target = target.parentNode;
|
||||
}
|
||||
this.$emit("rowClick", target.dataset.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.fs-contentmenu-list {
|
||||
.fs-contentmenu-item {
|
||||
padding: 8px 20px 8px 15px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: #ecf5ff;
|
||||
color: #66b1ff;
|
||||
}
|
||||
.fs-contentmenu-item-title {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div v-show="flag" class="fs-contextmenu" :style="style">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "FsContextmenu",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
x: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
y: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
flag: {
|
||||
get() {
|
||||
if (this.visible) {
|
||||
// 注册全局监听事件 [ 目前只考虑鼠标解除触发 ]
|
||||
window.addEventListener("mousedown", this.watchContextmenu);
|
||||
}
|
||||
return this.visible;
|
||||
},
|
||||
set(newVal) {
|
||||
this.$emit("update:visible", newVal);
|
||||
}
|
||||
},
|
||||
style() {
|
||||
return {
|
||||
left: this.x + "px",
|
||||
top: this.y + "px",
|
||||
display: this.visible ? "block" : "none "
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 将菜单放置到body下
|
||||
document.querySelector("body").appendChild(this.$el);
|
||||
},
|
||||
methods: {
|
||||
watchContextmenu(event) {
|
||||
if (!this.$el.contains(event.target) || event.button !== 0) this.flag = false;
|
||||
window.removeEventListener("mousedown", this.watchContextmenu);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.fs-contextmenu {
|
||||
position: absolute;
|
||||
padding: 5px 0;
|
||||
z-index: 2018;
|
||||
background: #fff;
|
||||
border: 1px solid #cfd7e5;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<a-dropdown class="fs-locale-picker">
|
||||
<fs-iconify icon="ion-globe-outline" @click.prevent></fs-iconify>
|
||||
<template #overlay>
|
||||
<a-menu @click="changeLocale">
|
||||
<a-menu-item v-for="item in languages" :key="item.key" :command="item.key">
|
||||
<div class="language-item">
|
||||
<span v-if="item.key === current" class="icon-radio">
|
||||
<span class="iconify" data-icon="ion:radio-button-on" data-inline="false"></span>
|
||||
</span>
|
||||
<span v-else class="icon-radio">
|
||||
<span class="iconify" data-icon="ion:radio-button-off" data-inline="false"></span>
|
||||
</span>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import i18n from "../../../i18n";
|
||||
import { computed, inject } from "vue";
|
||||
import _ from "lodash-es";
|
||||
export default {
|
||||
name: "FsLocale",
|
||||
setup() {
|
||||
const languages = computed(() => {
|
||||
const map = i18n.global.messages?.value || {};
|
||||
const list = [];
|
||||
_.forEach(map, (item, key) => {
|
||||
list.push({
|
||||
key,
|
||||
label: item.label
|
||||
});
|
||||
});
|
||||
return list;
|
||||
});
|
||||
const current = computed(() => {
|
||||
return i18n.global.locale.value;
|
||||
});
|
||||
|
||||
const routerReload = inject("fn:router.reload");
|
||||
const localeChanged = inject("fn:locale.changed");
|
||||
const changeLocale = (change) => {
|
||||
i18n.global.locale.value = change.key;
|
||||
routerReload();
|
||||
localeChanged(change.key)
|
||||
};
|
||||
return {
|
||||
languages,
|
||||
current,
|
||||
changeLocale
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.locale-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.language-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.icon-radio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.iconify {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,224 @@
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ref, watch, onMounted, onUnmounted, resolveComponent, nextTick, defineComponent } from "vue";
|
||||
import getEachDeep from "deepdash-es/getEachDeep";
|
||||
import _ from "lodash-es";
|
||||
import BScroll from "better-scroll";
|
||||
import "./index.less";
|
||||
const eachDeep = getEachDeep(_);
|
||||
|
||||
function useBetterScroll(enabled = true) {
|
||||
let bsRef = ref(null);
|
||||
let asideMenuRef = ref();
|
||||
|
||||
let onOpenChange = () => {};
|
||||
if (enabled) {
|
||||
function bsInit() {
|
||||
if (asideMenuRef.value == null) {
|
||||
return;
|
||||
}
|
||||
bsRef.value = new BScroll(asideMenuRef.value, {
|
||||
mouseWheel: true,
|
||||
click: true,
|
||||
momentum: false,
|
||||
// 如果你愿意可以打开显示滚动条
|
||||
scrollbar: {
|
||||
fade: true,
|
||||
interactive: false
|
||||
},
|
||||
bounce: false
|
||||
});
|
||||
}
|
||||
|
||||
function bsDestroy() {
|
||||
if (bsRef.value != null && bsRef.value.destroy) {
|
||||
try {
|
||||
bsRef.value.destroy();
|
||||
} catch (e) {
|
||||
// console.error(e);
|
||||
} finally {
|
||||
bsRef.value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
bsInit();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
bsDestroy();
|
||||
});
|
||||
onOpenChange = async () => {
|
||||
console.log("onOpenChange");
|
||||
setTimeout(() => {
|
||||
bsRef.value?.refresh();
|
||||
}, 300);
|
||||
};
|
||||
}
|
||||
return {
|
||||
onOpenChange,
|
||||
asideMenuRef
|
||||
};
|
||||
}
|
||||
export default defineComponent({
|
||||
name: "FsMenu",
|
||||
inheritAttrs: true,
|
||||
props: {
|
||||
menus: {},
|
||||
expandSelected: {
|
||||
default: false
|
||||
},
|
||||
scroll: {}
|
||||
},
|
||||
setup(props, ctx) {
|
||||
async function open(path) {
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
if (path.startsWith("http://") || path.startsWith("https://")) {
|
||||
window.open(path);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const navigationResult = await router.push(path);
|
||||
if (navigationResult) {
|
||||
// 导航被阻止
|
||||
} else {
|
||||
// 导航成功 (包括重新导航的情况)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("导航失败", e);
|
||||
}
|
||||
}
|
||||
function onSelect(item) {
|
||||
open(item.key);
|
||||
}
|
||||
|
||||
const FsIcon = resolveComponent("FsIcon");
|
||||
|
||||
const buildMenus = (children) => {
|
||||
const slots = [];
|
||||
if (children == null) {
|
||||
return slots;
|
||||
}
|
||||
for (let sub of children) {
|
||||
const title = () => {
|
||||
if (sub?.meta?.icon) {
|
||||
return (
|
||||
<div class={"menu-item-title"}>
|
||||
<FsIcon class={"anticon"} icon={sub.meta.icon} />
|
||||
<span>{sub.title}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return sub.title;
|
||||
};
|
||||
if (sub.children && sub.children.length > 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const subSlots = {
|
||||
default: () => {
|
||||
return buildMenus(sub.children);
|
||||
},
|
||||
title
|
||||
};
|
||||
function onTitleClick() {
|
||||
if (sub.path && ctx.attrs.mode === "horizontal") {
|
||||
open(sub.path);
|
||||
}
|
||||
}
|
||||
slots.push(<a-sub-menu key={sub.index} v-slots={subSlots} onTitleClick={onTitleClick} />);
|
||||
} else {
|
||||
slots.push(
|
||||
<a-menu-item key={sub.path} title={sub.title}>
|
||||
{title}
|
||||
</a-menu-item>
|
||||
);
|
||||
}
|
||||
}
|
||||
return slots;
|
||||
};
|
||||
const slots = {
|
||||
default() {
|
||||
return buildMenus(props.menus);
|
||||
}
|
||||
};
|
||||
const selectedKeys = ref([]);
|
||||
const openKeys = ref([]);
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
function openSelectedParents(fullPath) {
|
||||
if (!props.expandSelected) {
|
||||
return;
|
||||
}
|
||||
if (props.menus == null) {
|
||||
return;
|
||||
}
|
||||
const keys = [];
|
||||
let changed = false;
|
||||
eachDeep(props.menus, (value, key, parent, context) => {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
if (value.path === fullPath) {
|
||||
_.forEach(context.parents, (item) => {
|
||||
if (item.value instanceof Array) {
|
||||
return;
|
||||
}
|
||||
keys.push(item.value.index);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (keys.length > 0) {
|
||||
for (let key of keys) {
|
||||
if (openKeys.value.indexOf(key) === -1) {
|
||||
openKeys.value.push(key);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
const { asideMenuRef, onOpenChange } = useBetterScroll(props.scroll);
|
||||
|
||||
watch(
|
||||
() => {
|
||||
return route.fullPath;
|
||||
},
|
||||
(path) => {
|
||||
// path = route.fullPath;
|
||||
selectedKeys.value = [path];
|
||||
const changed = openSelectedParents(path);
|
||||
if (changed) {
|
||||
onOpenChange();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
const menu = (
|
||||
<a-menu
|
||||
mode={"inline"}
|
||||
theme={"light"}
|
||||
v-slots={slots}
|
||||
onClick={onSelect}
|
||||
onOpenChange={onOpenChange}
|
||||
v-models={[
|
||||
[openKeys.value, "openKeys"],
|
||||
[selectedKeys.value, "selectedKeys"]
|
||||
]}
|
||||
{...ctx.attrs}
|
||||
/>
|
||||
);
|
||||
const classNames = { "fs-menu-wrapper": true, "fs-menu-better-scroll": props.scroll };
|
||||
return (
|
||||
<div ref={asideMenuRef} class={classNames}>
|
||||
{menu}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
.fs-menu-wrapper{
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
&.fs-menu-better-scroll{
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.menu-item-title{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div v-if="showSourceLink" class="fs-source-link-group">
|
||||
<div class="fs-source-link" @click="goSource('https://gitee.com')">本页源码(Gitee)</div>
|
||||
<div class="fs-source-link" @click="goSource('https://github.com')">本页源码(Github)</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
export default defineComponent({
|
||||
name: "FsSourceLink",
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
const showSourceLink = ref(false);
|
||||
watch(
|
||||
() => {
|
||||
return router.currentRoute.value.fullPath;
|
||||
},
|
||||
(value) => {
|
||||
showSourceLink.value = value !== "/index";
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
const middle = "/fast-crud/fs-admin-antdv/tree/main/src/views";
|
||||
function goSource(prefix) {
|
||||
const path = router.currentRoute.value.fullPath;
|
||||
window.open(prefix + middle + path + "/index.vue");
|
||||
}
|
||||
return {
|
||||
goSource,
|
||||
showSourceLink
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.fs-source-link-group {
|
||||
position: fixed;
|
||||
right: 3px;
|
||||
bottom: 20px;
|
||||
.fs-source-link {
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
border-radius: 5px 0 0 5px;
|
||||
padding: 5px;
|
||||
background: #666;
|
||||
color: #fff;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<div class="fs-multiple-page-control-group">
|
||||
<div class="fs-multiple-page-control-content">
|
||||
<div class="fs-multiple-page-control-content-inner">
|
||||
<a-tabs
|
||||
class="fs-multiple-page-control fs-multiple-page-sort"
|
||||
:active-key="page.getCurrent"
|
||||
type="editable-card"
|
||||
hide-add
|
||||
@tabClick="handleClick"
|
||||
@edit="handleTabEdit"
|
||||
@contextmenu="handleContextmenu"
|
||||
>
|
||||
<a-tab-pane
|
||||
v-for="item in page.getOpened"
|
||||
:key="item.fullPath"
|
||||
:tab="item.meta?.title || '未命名'"
|
||||
:name="item.fullPath"
|
||||
:closable="isTabClosable(item)"
|
||||
/>
|
||||
</a-tabs>
|
||||
<!-- <fs-contextmenu v-model:visible="contextmenuFlag" :x="contentmenuX" :y="contentmenuY">-->
|
||||
<!-- <fs-contextmenu-list-->
|
||||
<!-- :menulist="tagName === '/index' ? contextmenuListIndex : contextmenuList"-->
|
||||
<!-- @rowClick="contextmenuClick"-->
|
||||
<!-- />-->
|
||||
<!-- </fs-contextmenu>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fs-multiple-page-control-btn">
|
||||
<a-dropdown-button class="control-btn-dropdown" split-button @click="closeAll">
|
||||
<span class="iconify" data-icon="ion:close-circle" data-inline="false"></span>
|
||||
<template #icon><DownOutlined /></template>
|
||||
<template #overlay>
|
||||
<a-menu @click="(command) => handleControlItemClick(command)">
|
||||
<a-menu-item key="left">
|
||||
<fs-icon name="arrow-left" class="fs-mr-10" />
|
||||
关闭左侧
|
||||
</a-menu-item>
|
||||
<a-menu-item key="right">
|
||||
<fs-icon name="arrow-right" class="fs-mr-10" />
|
||||
关闭右侧
|
||||
</a-menu-item>
|
||||
<a-menu-item key="other">
|
||||
<fs-icon name="times" class="fs-mr-10" />
|
||||
关闭其它
|
||||
</a-menu-item>
|
||||
<a-menu-item key="all">
|
||||
<fs-icon name="times-circle" class="fs-mr-10" />
|
||||
全部关闭
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Sortable from "sortablejs";
|
||||
import { usePageStore } from "../../../store/modules/page";
|
||||
import { computed } from "vue";
|
||||
export default {
|
||||
name: "FsTabs",
|
||||
components: {
|
||||
// FsContextmenu: () => import("../contextmenu/index.vue"),
|
||||
// FsContextmenuList: () => import("../contextmenu/components/contentmenuList/index.vue")
|
||||
},
|
||||
setup() {
|
||||
const pageStore = usePageStore();
|
||||
|
||||
const actions = {
|
||||
close: pageStore.close,
|
||||
closeLeft: pageStore.closeLeft,
|
||||
closeRight: pageStore.closeRight,
|
||||
closeOther: pageStore.closeOther,
|
||||
closeAll: pageStore.closeAll,
|
||||
openedSort: pageStore.openedSort
|
||||
};
|
||||
console.log("opened", pageStore.getOpened);
|
||||
const computeOpened = computed(() => {
|
||||
console.log("opened", pageStore.getOpened);
|
||||
return pageStore.getOpened;
|
||||
});
|
||||
|
||||
return {
|
||||
page: pageStore,
|
||||
...actions,
|
||||
computeOpened
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
contextmenuFlag: false,
|
||||
contentmenuX: 0,
|
||||
contentmenuY: 0,
|
||||
contextmenuListIndex: [{ icon: "times-circle", title: "关闭全部", value: "all" }],
|
||||
contextmenuList: [
|
||||
{ icon: "arrow-left", title: "关闭左侧", value: "left" },
|
||||
{ icon: "arrow-right", title: "关闭右侧", value: "right" },
|
||||
{ icon: "times", title: "关闭其它", value: "other" },
|
||||
{ icon: "times-circle", title: "关闭全部", value: "all" }
|
||||
],
|
||||
tagName: "/index"
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const el = document.querySelectorAll(".fs-multiple-page-sort .el-tabs__nav")[0];
|
||||
// Sortable.create(el, {
|
||||
// onEnd: (evt) => {
|
||||
// const { oldIndex, newIndex } = evt;
|
||||
// this.openedSort({ oldIndex, newIndex });
|
||||
// }
|
||||
// });
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @description 计算某个标签页是否可关闭
|
||||
* @param {Object} page 其中一个标签页
|
||||
*/
|
||||
isTabClosable(page) {
|
||||
return page.name !== "index";
|
||||
},
|
||||
/**
|
||||
* @description 右键菜单功能点击
|
||||
* @param {Object} event 事件
|
||||
*/
|
||||
handleContextmenu(event) {
|
||||
let target = event.target;
|
||||
// fix https://github.com/fs-projects/fs-admin/issues/54
|
||||
let flag = false;
|
||||
if (target.className.indexOf("el-tabs__item") > -1) flag = true;
|
||||
else if (target.parentNode.className.indexOf("el-tabs__item") > -1) {
|
||||
target = target.parentNode;
|
||||
flag = true;
|
||||
}
|
||||
if (flag) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.contentmenuX = event.clientX;
|
||||
this.contentmenuY = event.clientY;
|
||||
this.tagName = target.getAttribute("aria-controls").slice(5);
|
||||
this.contextmenuFlag = true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description 右键菜单的 row-click 事件
|
||||
* @param {String} command 事件类型
|
||||
*/
|
||||
contextmenuClick(command) {
|
||||
this.handleControlItemClick(command, this.tagName);
|
||||
},
|
||||
/**
|
||||
* @description 接收点击关闭控制上选项的事件
|
||||
* @param {String} command 事件类型
|
||||
* @param {String} tagName tab 名称
|
||||
*/
|
||||
handleControlItemClick(command, tagName = null) {
|
||||
//if (tagName) this.contextmenuFlag = false;
|
||||
const params = { pageSelect: tagName };
|
||||
switch (command.key) {
|
||||
case "left":
|
||||
this.closeLeft(params);
|
||||
break;
|
||||
case "right":
|
||||
this.closeRight(params);
|
||||
break;
|
||||
case "other":
|
||||
this.closeOther(params);
|
||||
break;
|
||||
case "all":
|
||||
this.closeAll();
|
||||
break;
|
||||
default:
|
||||
this.$message.error("无效的操作");
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description 接收点击 tab 标签的事件
|
||||
* @param {object} tab 标签
|
||||
* @param {object} event 事件
|
||||
*/
|
||||
handleClick(tab) {
|
||||
// 找到点击的页面在 tag 列表里是哪个
|
||||
const page = this.page.getOpened.find((page) => page.fullPath === tab);
|
||||
if (page) {
|
||||
const { name, params, query } = page;
|
||||
this.$router.push({ name, params, query });
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description 点击 tab 上的删除按钮触发这里
|
||||
* @param {String} tagName tab 名称
|
||||
*/
|
||||
handleTabEdit(tagName, action) {
|
||||
if (action === "remove") {
|
||||
this.close({ tagName });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="less">
|
||||
//common
|
||||
.fs-multiple-page-control-group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
.fs-multiple-page-control-content {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.fs-multiple-page-control-btn {
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
//antdv
|
||||
.fs-multiple-page-control-group {
|
||||
.ant-tabs-bar {
|
||||
margin: 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.ant-tabs-top > .ant-tabs-nav,
|
||||
.ant-tabs-bottom > .ant-tabs-nav,
|
||||
.ant-tabs-top > div > .ant-tabs-nav,
|
||||
.ant-tabs-bottom > div > .ant-tabs-nav {
|
||||
margin: 0;
|
||||
}
|
||||
.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-nav {
|
||||
.ant-tabs-tab {
|
||||
margin-right: 0;
|
||||
border-right: 0;
|
||||
|
||||
&:first-of-type {
|
||||
border-top-left-radius: 2px;
|
||||
}
|
||||
&:last-of-type {
|
||||
border-top-right-radius: 2px;
|
||||
border-right: 1px;
|
||||
}
|
||||
|
||||
&:not(.ant-tabs-tab-active) {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
.ant-tabs-tab-active {
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
}
|
||||
.ant-tabs-close-x {
|
||||
display: none;
|
||||
}
|
||||
.ant-tabs-tab {
|
||||
&:hover {
|
||||
.ant-tabs-close-x {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-tabs-tab-active {
|
||||
.ant-tabs-close-x {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.fs-multiple-page-control-btn {
|
||||
display: flex;
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
height: 100%;
|
||||
color: #666;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.control-btn-dropdown {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-arrow-show {
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
.ant-tabs-tab-prev {
|
||||
border-right: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.ant-tabs-tab-next {
|
||||
border-left: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
//element
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="fs-theme-color-picker">
|
||||
<h4>主题色</h4>
|
||||
<div class="fs-theme-colors">
|
||||
<a-tooltip v-for="(item, index) in colorList" :key="index" class="fs-theme-color-item">
|
||||
<template #title>
|
||||
{{ item.key }}
|
||||
</template>
|
||||
<a-tag :color="item.color" @click="changeColor(item.color)">
|
||||
<CheckOutlined v-if="item.color === primaryColor" />
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref } from "vue";
|
||||
const colorListDefine = [
|
||||
{
|
||||
key: "薄暮",
|
||||
color: "#f5222d"
|
||||
},
|
||||
{
|
||||
key: "火山",
|
||||
color: "#fa541c"
|
||||
},
|
||||
{
|
||||
key: "日暮",
|
||||
color: "#faad14"
|
||||
},
|
||||
{
|
||||
key: "明青",
|
||||
color: "#13c2c2"
|
||||
},
|
||||
{
|
||||
key: "极光绿",
|
||||
color: "#52c41a"
|
||||
},
|
||||
{
|
||||
key: "拂晓蓝(默认)",
|
||||
color: "#1890ff"
|
||||
},
|
||||
{
|
||||
key: "极客蓝",
|
||||
color: "#2f54eb"
|
||||
},
|
||||
{
|
||||
key: "酱紫",
|
||||
color: "#722ed1"
|
||||
}
|
||||
];
|
||||
export default defineComponent({
|
||||
name: "FsThemeColorPicker",
|
||||
props: {
|
||||
primaryColor: {
|
||||
default: "#1890ff"
|
||||
}
|
||||
},
|
||||
emits: ["change"],
|
||||
setup(props, ctx) {
|
||||
const colorList = ref(colorListDefine);
|
||||
function changeColor(color) {
|
||||
ctx.emit("change", color);
|
||||
}
|
||||
return {
|
||||
colorList,
|
||||
changeColor
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.fs-theme-color-picker {
|
||||
.fs-theme-colors {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
.fs-theme-color-item {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="fs-theme" @click="show()">
|
||||
<fs-iconify icon="ion:sparkles-outline" />
|
||||
<a-drawer
|
||||
v-model:visible="visible"
|
||||
title="主题设置"
|
||||
placement="right"
|
||||
width="350px"
|
||||
:closable="false"
|
||||
@after-visible-change="afterVisibleChange"
|
||||
>
|
||||
<fs-theme-color-picker
|
||||
:primary-color="setting.getTheme.primaryColor"
|
||||
@change="setting.setPrimaryColor"
|
||||
></fs-theme-color-picker>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, defineComponent } from "vue";
|
||||
import FsThemeColorPicker from "./color-picker.vue";
|
||||
import { useSettingStore } from "/@/store/modules/settings";
|
||||
|
||||
export default defineComponent({
|
||||
name: "FsTheme",
|
||||
components: { FsThemeColorPicker },
|
||||
setup() {
|
||||
const visible = ref(false);
|
||||
function afterVisibleChange() {}
|
||||
function show() {
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
const setting = useSettingStore();
|
||||
return {
|
||||
visible,
|
||||
show,
|
||||
afterVisibleChange,
|
||||
setting
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.fs-theme {
|
||||
}
|
||||
.fs-theme-drawer {
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<a-dropdown>
|
||||
<div class="fs-user-info">您好,{{ userStore.getUserInfo?.nickName }}</div>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item>
|
||||
<div @click="doLogout">注销登录</div>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import { useUserStore } from "/src/store/modules/user";
|
||||
import { Modal } from "ant-design-vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
export default defineComponent({
|
||||
name: "FsUserInfo",
|
||||
setup() {
|
||||
const userStore = useUserStore();
|
||||
console.log("user", userStore);
|
||||
const { t } = useI18n();
|
||||
function doLogout() {
|
||||
Modal.confirm({
|
||||
iconType: "warning",
|
||||
title: t("app.login.logoutTip"),
|
||||
content: t("app.login.logoutMessage"),
|
||||
onOk: async () => {
|
||||
await userStore.logout(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
userStore,
|
||||
doLogout
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,230 @@
|
||||
<template xmlns:w="http://www.w3.org/1999/xhtml">
|
||||
<a-layout class="fs-framework">
|
||||
<a-layout-sider v-model:collapsed="asideCollapsed" :trigger="null" collapsible>
|
||||
<div class="header-logo">
|
||||
<img src="/images/logo/rect-black.svg" />
|
||||
<span v-if="!asideCollapsed" class="title">FsAdmin</span>
|
||||
</div>
|
||||
<div class="aside-menu">
|
||||
<fs-menu :scroll="true" :menus="asideMenus" :expand-selected="!asideCollapsed" />
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<a-layout class="layout-body">
|
||||
<a-layout-header class="header">
|
||||
<div class="header-buttons">
|
||||
<div class="menu-fold" @click="asideCollapsedToggle">
|
||||
<MenuUnfoldOutlined v-if="asideCollapsed" />
|
||||
<MenuFoldOutlined v-else />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fs-menu
|
||||
class="header-menu"
|
||||
mode="horizontal"
|
||||
:expand-selected="false"
|
||||
:selectable="false"
|
||||
:menus="frameworkMenus"
|
||||
/>
|
||||
<div class="header-right header-buttons">
|
||||
<!-- <button-->
|
||||
<!-- w:bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"-->
|
||||
<!-- w:text="sm white"-->
|
||||
<!-- w:font="mono light"-->
|
||||
<!-- w:p="y-2 x-4"-->
|
||||
<!-- w:border="2 rounded blue-200"-->
|
||||
<!-- >-->
|
||||
<!-- Button-->
|
||||
<!-- </button>-->
|
||||
<fs-menu
|
||||
class="header-menu"
|
||||
mode="horizontal"
|
||||
:expand-selected="false"
|
||||
:selectable="false"
|
||||
:menus="headerMenus"
|
||||
/>
|
||||
<fs-locale class="btn" />
|
||||
<!-- <fs-theme-set class="btn" />-->
|
||||
<fs-user-info class="btn" />
|
||||
</div>
|
||||
</a-layout-header>
|
||||
<fs-tabs></fs-tabs>
|
||||
<a-layout-content class="fs-framework-content">
|
||||
<router-view>
|
||||
<template #default="{ Component, route }">
|
||||
<transition name="fade-transverse">
|
||||
<keep-alive :include="keepAlive">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</template>
|
||||
</router-view>
|
||||
</a-layout-content>
|
||||
<a-layout-footer class="fs-framework-footer"
|
||||
>by fast-crud
|
||||
<fs-source-link />
|
||||
</a-layout-footer>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, onErrorCaptured, ref } from "vue";
|
||||
import FsMenu from "./components/menu/index.jsx";
|
||||
import FsLocale from "./components/locale/index.vue";
|
||||
import FsSourceLink from "./components/source-link/index.vue";
|
||||
import FsUserInfo from "./components/user-info/index.vue";
|
||||
import FsTabs from "./components/tabs/index.vue";
|
||||
import { useResourceStore } from "../store/modules/resource";
|
||||
import { usePageStore } from "/@/store/modules/page";
|
||||
import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons-vue";
|
||||
import FsThemeSet from "/@/layout/components/theme/index.vue";
|
||||
import { notification } from "ant-design-vue";
|
||||
export default {
|
||||
name: "LayoutFramework",
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: { FsThemeSet, MenuFoldOutlined, MenuUnfoldOutlined, FsMenu, FsLocale, FsSourceLink, FsUserInfo, FsTabs },
|
||||
setup() {
|
||||
const resourceStore = useResourceStore();
|
||||
const frameworkMenus = computed(() => {
|
||||
return resourceStore.getFrameworkMenus;
|
||||
});
|
||||
const headerMenus = computed(() => {
|
||||
return resourceStore.getHeaderMenus;
|
||||
});
|
||||
const asideMenus = computed(() => {
|
||||
return resourceStore.getAsideMenus;
|
||||
});
|
||||
|
||||
const pageStore = usePageStore();
|
||||
const keepAlive = pageStore.keepAlive;
|
||||
|
||||
const asideCollapsed = ref(false);
|
||||
function asideCollapsedToggle() {
|
||||
asideCollapsed.value = !asideCollapsed.value;
|
||||
}
|
||||
onErrorCaptured((e) => {
|
||||
console.error("ErrorCaptured:", e);
|
||||
notification.error({ message: e.message });
|
||||
//阻止错误向上传递
|
||||
return false;
|
||||
});
|
||||
return {
|
||||
frameworkMenus,
|
||||
headerMenus,
|
||||
asideMenus,
|
||||
keepAlive,
|
||||
asideCollapsed,
|
||||
asideCollapsedToggle
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import "../style/theme/index.less";
|
||||
.fs-framework {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
.header-logo {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
// margin: 16px 24px 16px 0;
|
||||
//background: rgba(255, 255, 255, 0.3);
|
||||
img {
|
||||
height: 80%;
|
||||
}
|
||||
.title {
|
||||
margin-left: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.fs-framework-content {
|
||||
flex: 1;
|
||||
border-left: 1px solid #f0f0f0;
|
||||
}
|
||||
.fs-framework-footer {
|
||||
border-left: 1px solid #f0f0f0;
|
||||
padding: 10px 20px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-size: 14px;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& > * {
|
||||
cursor: pointer;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
& > .btn {
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
.header-right {
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
.header-menu {
|
||||
flex: 1;
|
||||
}
|
||||
.aside-menu {
|
||||
flex: 1;
|
||||
ui {
|
||||
height: 100%;
|
||||
}
|
||||
overflow: hidden;
|
||||
// overflow-y: auto;
|
||||
}
|
||||
|
||||
.layout-body {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
//antdv
|
||||
.fs-framework {
|
||||
&.ant-layout {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.ant-layout-sider-children {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ant-layout-sider {
|
||||
// border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
.ant-layout-header {
|
||||
height: 50px;
|
||||
padding: 0 10px;
|
||||
line-height: 50px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
.ant-layout-content {
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
//element
|
||||
.fs-framework {
|
||||
.el-aside {
|
||||
.el-menu {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div id="userLayout" :class="['user-layout-wrapper']">
|
||||
<div class="login-container flex-center">
|
||||
<div class="user-layout-lang"></div>
|
||||
<div class="user-layout-content">
|
||||
<div class="top flex flex-col items-center justify-center">
|
||||
<div class="header flex flex-row items-center">
|
||||
<img src="/images/logo/rect-black.svg" class="logo" alt="logo" />
|
||||
<span class="title">FsAdmin</span>
|
||||
</div>
|
||||
<div class="desc">fast-crud,开发crud快如闪电</div>
|
||||
</div>
|
||||
|
||||
<router-view />
|
||||
|
||||
<div class="footer">
|
||||
<div class="links">
|
||||
<a href="_self">帮助</a>
|
||||
<a href="_self">隐私</a>
|
||||
<a href="_self">条款</a>
|
||||
</div>
|
||||
<div class="copyright">Copyright © 2021 Greper</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "LayoutOutside"
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#userLayout.user-layout-wrapper {
|
||||
height: 100%;
|
||||
|
||||
&.mobile {
|
||||
.container {
|
||||
.main {
|
||||
max-width: 368px;
|
||||
width: 98%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-container {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
background: #f0f2f5 url(/src/assets/background.svg) no-repeat 50%;
|
||||
background-size: 100%;
|
||||
//padding: 50px 0 84px;
|
||||
position: relative;
|
||||
|
||||
.user-layout-lang {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 44px;
|
||||
text-align: right;
|
||||
|
||||
.select-lang-trigger {
|
||||
cursor: pointer;
|
||||
padding: 12px;
|
||||
margin-right: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.user-layout-content {
|
||||
padding: 32px 0 24px;
|
||||
|
||||
.top {
|
||||
text-align: center;
|
||||
|
||||
.header {
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
margin-left: -12px;
|
||||
margin-top: -10px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 44px;
|
||||
vertical-align: top;
|
||||
margin-right: 16px;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 33px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-family: Avenir, "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
.desc {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
margin-top: 12px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
min-width: 260px;
|
||||
width: 368px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
// position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
padding: 0 16px;
|
||||
margin: 48px 0 24px;
|
||||
text-align: center;
|
||||
|
||||
.links {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
a {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
transition: all 0.3s;
|
||||
&:not(:last-child) {
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.copyright {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "LayoutPass"
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import Antd from "ant-design-vue";
|
||||
import "ant-design-vue/dist/antd.less";
|
||||
// import "virtual:windi.css";
|
||||
import "./style/common.less";
|
||||
import "./mock";
|
||||
import i18n from "./i18n";
|
||||
import store from "./store";
|
||||
|
||||
import plugin from "./plugin/";
|
||||
import components from "./components";
|
||||
// @ts-ignore
|
||||
const app = createApp(App);
|
||||
// 尽量
|
||||
app.use(Antd);
|
||||
app.use(router);
|
||||
app.use(i18n);
|
||||
app.use(store);
|
||||
app.use(plugin, { i18n });
|
||||
app.use(components);
|
||||
app.mount("#app");
|
||||
@@ -0,0 +1,280 @@
|
||||
import _ from "lodash-es";
|
||||
function copyList(originList, newList, options, parentId) {
|
||||
for (const item of originList) {
|
||||
const newItem = { ...item, parentId };
|
||||
newItem.id = ++options.idGenerator;
|
||||
newList.push(newItem);
|
||||
if (item.children != null) {
|
||||
newItem.children = [];
|
||||
copyList(item.children, newItem.children, options, newItem.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function delById(req, list) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i];
|
||||
console.log("remove i", i, req, req.params.id, item.id);
|
||||
if (item.id === parseInt(req.params.id)) {
|
||||
console.log("remove i", i);
|
||||
list.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
if (item.children != null && item.children.length > 0) {
|
||||
delById(req, item.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findById(id, list) {
|
||||
for (const item of list) {
|
||||
if (item.id === id) {
|
||||
return item;
|
||||
}
|
||||
if (item.children != null && item.children.length > 0) {
|
||||
const sub = findById(id, item.children);
|
||||
if (sub != null) {
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export default {
|
||||
findById,
|
||||
buildMock(options) {
|
||||
const name = options.name;
|
||||
if (options.copyTimes == null) {
|
||||
options.copyTimes = 29;
|
||||
}
|
||||
const list = [];
|
||||
for (let i = 0; i < options.copyTimes; i++) {
|
||||
copyList(options.list, list, options);
|
||||
}
|
||||
options.list = list;
|
||||
return [
|
||||
{
|
||||
path: "/mock/" + name + "/page",
|
||||
method: "get",
|
||||
handle(req) {
|
||||
let data = [...list];
|
||||
let limit = 20;
|
||||
let offset = 0;
|
||||
for (const item of list) {
|
||||
if (item.children != null && item.children.length === 0) {
|
||||
item.hasChildren = false;
|
||||
item.lazy = false;
|
||||
}
|
||||
}
|
||||
let orderProp, orderAsc;
|
||||
if (req && req.body) {
|
||||
const { page, query, sort } = req.body;
|
||||
if (page.limit != null) {
|
||||
limit = parseInt(page.limit);
|
||||
}
|
||||
if (page.offset != null) {
|
||||
offset = parseInt(page.offset);
|
||||
}
|
||||
orderProp = sort.prop;
|
||||
orderAsc = sort.asc;
|
||||
|
||||
if (Object.keys(query).length > 0) {
|
||||
data = list.filter((item) => {
|
||||
let allFound = true; // 是否所有条件都符合
|
||||
for (const key in query) {
|
||||
// 判定某一个条件
|
||||
const value = query[key];
|
||||
if (value == null || value === "") {
|
||||
continue;
|
||||
}
|
||||
if (value instanceof Array) {
|
||||
// 如果条件中的value是数组的话,只要查到一个就行
|
||||
if (value.length === 0) {
|
||||
continue;
|
||||
}
|
||||
let found = false;
|
||||
for (const i of value) {
|
||||
if (item[key] instanceof Array) {
|
||||
for (const j of item[key]) {
|
||||
if (i === j) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
} else if (item[key] === i || (typeof item[key] === "string" && item[key].indexOf(i + "") >= 0)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
allFound = false;
|
||||
}
|
||||
} else if (value instanceof Object) {
|
||||
for (const key2 in value) {
|
||||
const v = value[key2];
|
||||
if (v && item[key] && v !== item[key][key2]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (item[key] !== value) {
|
||||
allFound = false;
|
||||
}
|
||||
}
|
||||
return allFound;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const start = offset;
|
||||
let end = offset + limit;
|
||||
if (data.length < end) {
|
||||
end = data.length;
|
||||
}
|
||||
|
||||
if (orderProp) {
|
||||
// 排序
|
||||
data.sort((a, b) => {
|
||||
let ret = 0;
|
||||
if (a[orderProp] > b[orderProp]) {
|
||||
ret = 1;
|
||||
} else {
|
||||
ret = -1;
|
||||
}
|
||||
return orderAsc ? ret : -ret;
|
||||
});
|
||||
}
|
||||
|
||||
const records = data.slice(start, end);
|
||||
const lastOffset = data.length - (data.length % limit);
|
||||
if (offset > lastOffset) {
|
||||
offset = lastOffset;
|
||||
}
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: {
|
||||
records: records,
|
||||
total: data.length,
|
||||
limit,
|
||||
offset
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/mock/" + name + "/get",
|
||||
method: "get",
|
||||
handle(req) {
|
||||
let id = req.params.id;
|
||||
id = parseInt(id);
|
||||
let current = null;
|
||||
for (const item of list) {
|
||||
if (item.id === id) {
|
||||
current = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: current
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/mock/" + name + "/add",
|
||||
method: "post",
|
||||
handle(req) {
|
||||
req.body.id = ++options.idGenerator;
|
||||
list.unshift(req.body);
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: req.body.id
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/mock/" + name + "/update",
|
||||
method: "post",
|
||||
handle(req) {
|
||||
const item = findById(req.body.id, list);
|
||||
if (item) {
|
||||
_.mergeWith(item, req.body, (objValue, srcValue) => {
|
||||
if (srcValue == null) {
|
||||
return;
|
||||
}
|
||||
// 如果被合并对象为数组,则直接被覆盖对象覆盖,只要覆盖对象不为空
|
||||
if (_.isArray(objValue)) {
|
||||
return srcValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/mock/" + name + "/delete",
|
||||
method: "post",
|
||||
handle(req) {
|
||||
delById(req, list);
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/mock/" + name + "/batchDelete",
|
||||
method: "post",
|
||||
handle(req) {
|
||||
const ids = req.body.ids;
|
||||
for (let i = list.length - 1; i >= 0; i--) {
|
||||
const item = list[i];
|
||||
if (ids.indexOf(item.id) >= 0) {
|
||||
list.splice(i, 1);
|
||||
}
|
||||
}
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/mock/" + name + "/delete",
|
||||
method: "post",
|
||||
handle(req) {
|
||||
delById(req, list);
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/mock/" + name + "/all",
|
||||
method: "post",
|
||||
handle(req) {
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: list
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,268 @@
|
||||
export default [
|
||||
{
|
||||
value: "zhinan",
|
||||
label: "指南",
|
||||
children: [
|
||||
{
|
||||
value: "shejiyuanze",
|
||||
label: "设计原则",
|
||||
children: [
|
||||
{
|
||||
value: "yizhi",
|
||||
label: "一致"
|
||||
},
|
||||
{
|
||||
value: "fankui",
|
||||
label: "反馈"
|
||||
},
|
||||
{
|
||||
value: "xiaolv",
|
||||
label: "效率"
|
||||
},
|
||||
{
|
||||
value: "kekong",
|
||||
label: "可控"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "daohang",
|
||||
label: "导航",
|
||||
children: [
|
||||
{
|
||||
value: "cexiangdaohang",
|
||||
label: "侧向导航"
|
||||
},
|
||||
{
|
||||
value: "dingbudaohang",
|
||||
label: "顶部导航"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "zujian",
|
||||
label: "组件",
|
||||
children: [
|
||||
{
|
||||
value: "basic",
|
||||
label: "Basic",
|
||||
children: [
|
||||
{
|
||||
value: "layout",
|
||||
label: "Layout 布局"
|
||||
},
|
||||
{
|
||||
value: "color",
|
||||
label: "Color 色彩"
|
||||
},
|
||||
{
|
||||
value: "typography",
|
||||
label: "Typography 字体"
|
||||
},
|
||||
{
|
||||
value: "icon",
|
||||
label: "Icon 图标"
|
||||
},
|
||||
{
|
||||
value: "button",
|
||||
label: "Button 按钮"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "form",
|
||||
label: "Form",
|
||||
children: [
|
||||
{
|
||||
value: "radio",
|
||||
label: "Radio 单选框"
|
||||
},
|
||||
{
|
||||
value: "checkbox",
|
||||
label: "Checkbox 多选框"
|
||||
},
|
||||
{
|
||||
value: "input",
|
||||
label: "Input 输入框"
|
||||
},
|
||||
{
|
||||
value: "input-number",
|
||||
label: "InputNumber 计数器"
|
||||
},
|
||||
{
|
||||
value: "select",
|
||||
label: "Select 选择器"
|
||||
},
|
||||
{
|
||||
value: "cascader",
|
||||
label: "Cascader 级联选择器"
|
||||
},
|
||||
{
|
||||
value: "switch",
|
||||
label: "Switch 开关"
|
||||
},
|
||||
{
|
||||
value: "slider",
|
||||
label: "Slider 滑块"
|
||||
},
|
||||
{
|
||||
value: "time-picker",
|
||||
label: "TimePicker 时间选择器"
|
||||
},
|
||||
{
|
||||
value: "date-picker",
|
||||
label: "DatePicker 日期选择器"
|
||||
},
|
||||
{
|
||||
value: "datetime-picker",
|
||||
label: "DateTimePicker 日期时间选择器"
|
||||
},
|
||||
{
|
||||
value: "upload",
|
||||
label: "Upload 上传"
|
||||
},
|
||||
{
|
||||
value: "rate",
|
||||
label: "Rate 评分"
|
||||
},
|
||||
{
|
||||
value: "form1",
|
||||
label: "Form 表单"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "data",
|
||||
label: "Data",
|
||||
children: [
|
||||
{
|
||||
value: "table",
|
||||
label: "Table 表格"
|
||||
},
|
||||
{
|
||||
value: "tag",
|
||||
label: "Tag 标签"
|
||||
},
|
||||
{
|
||||
value: "progress",
|
||||
label: "Progress 进度条"
|
||||
},
|
||||
{
|
||||
value: "tree",
|
||||
label: "Tree 树形控件"
|
||||
},
|
||||
{
|
||||
value: "pagination",
|
||||
label: "Pagination 分页"
|
||||
},
|
||||
{
|
||||
value: "badge",
|
||||
label: "Badge 标记"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "notice",
|
||||
label: "Notice",
|
||||
children: [
|
||||
{
|
||||
value: "alert",
|
||||
label: "Alert 警告"
|
||||
},
|
||||
{
|
||||
value: "loading",
|
||||
label: "Loading 加载"
|
||||
},
|
||||
{
|
||||
value: "message",
|
||||
label: "Message 消息提示"
|
||||
},
|
||||
{
|
||||
value: "message-box",
|
||||
label: "MessageBox 弹框"
|
||||
},
|
||||
{
|
||||
value: "notification",
|
||||
label: "Notification 通知"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "navigation",
|
||||
label: "Navigation",
|
||||
children: [
|
||||
{
|
||||
value: "menu",
|
||||
label: "NavMenu 导航菜单"
|
||||
},
|
||||
{
|
||||
value: "tabs",
|
||||
label: "Tabs 标签页"
|
||||
},
|
||||
{
|
||||
value: "breadcrumb",
|
||||
label: "Breadcrumb 面包屑"
|
||||
},
|
||||
{
|
||||
value: "dropdown",
|
||||
label: "Dropdown 下拉菜单"
|
||||
},
|
||||
{
|
||||
value: "steps",
|
||||
label: "Steps 步骤条"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "others",
|
||||
label: "Others",
|
||||
children: [
|
||||
{
|
||||
value: "dialog",
|
||||
label: "Dialog 对话框"
|
||||
},
|
||||
{
|
||||
value: "tooltip",
|
||||
label: "Tooltip 文字提示"
|
||||
},
|
||||
{
|
||||
value: "popover",
|
||||
label: "Popover 弹出框"
|
||||
},
|
||||
{
|
||||
value: "card",
|
||||
label: "Card 卡片"
|
||||
},
|
||||
{
|
||||
value: "carousel",
|
||||
label: "Carousel 走马灯"
|
||||
},
|
||||
{
|
||||
value: "collapse",
|
||||
label: "Collapse 折叠面板"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "ziyuan",
|
||||
label: "资源",
|
||||
children: [
|
||||
{
|
||||
value: "axure",
|
||||
label: "Axure Components"
|
||||
},
|
||||
{
|
||||
value: "sketch",
|
||||
label: "Sketch Templates"
|
||||
},
|
||||
{
|
||||
value: "jiaohu",
|
||||
label: "组件交互文档"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,123 @@
|
||||
import cascaderData from "./cascader-data";
|
||||
import pcaDataLittle from "./pca-data-little";
|
||||
import { TreeNodesLazyLoader, getPcaData } from "./pcas-data";
|
||||
|
||||
const openStatus = [
|
||||
{ value: "1", label: "打开", color: "success",icon:"ion:radio-button-on" },
|
||||
{ value: "2", label: "停止", color: "cyan" },
|
||||
{ value: "0", label: "关闭", color: "red",icon:"ion:radio-button-off" }
|
||||
];
|
||||
|
||||
const moreOpenStatus = [
|
||||
{ value: "1", label: "打开(open)", color: "success" },
|
||||
{ value: "2", label: "停止(stop)", color: "cyan" },
|
||||
{ value: "0", label: "关闭(close)", color: "red" }
|
||||
];
|
||||
|
||||
const textStatus = [
|
||||
{ id: "1", text: "打开", color: "success" },
|
||||
{ id: "2", text: "停止", color: "cyan" },
|
||||
{ id: "0", text: "关闭", color: "red" }
|
||||
];
|
||||
|
||||
export function GetTreeChildrenByParentId(parentId) {
|
||||
return TreeNodesLazyLoader.getChildren(parentId);
|
||||
}
|
||||
|
||||
export function GetNodesByValues(values) {
|
||||
return TreeNodesLazyLoader.getNodesByValues(values);
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
path: "/mock/dicts/OpenStatusEnum",
|
||||
method: "get",
|
||||
handle() {
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: openStatus
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/mock/dicts/_OpenStatusEnum2",
|
||||
method: "get",
|
||||
handle() {
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: textStatus
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/mock/dicts/moreOpenStatusEnum",
|
||||
method: "get",
|
||||
handle() {
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: moreOpenStatus
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/mock/dicts/cascaderData",
|
||||
method: "get",
|
||||
handle() {
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: cascaderData
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/mock/dicts/pca",
|
||||
method: "get",
|
||||
async handle() {
|
||||
const data = await getPcaData();
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: data
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/mock/dicts/littlePca",
|
||||
method: "get",
|
||||
async handle() {
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: pcaDataLittle
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/mock/tree/GetTreeChildrenByParentId",
|
||||
method: "get",
|
||||
async handle({ params }) {
|
||||
const list = await GetTreeChildrenByParentId(params.parentId);
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: list
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/mock/tree/GetNodesByValues",
|
||||
method: "get",
|
||||
async handle({ params }) {
|
||||
const list = await GetNodesByValues(params.values);
|
||||
return {
|
||||
code: 0,
|
||||
msg: "success",
|
||||
data: list
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,70 @@
|
||||
export default [
|
||||
{
|
||||
code: "1",
|
||||
name: "北京",
|
||||
children: [
|
||||
{
|
||||
code: "2",
|
||||
name: "北京市区",
|
||||
children: [
|
||||
{
|
||||
code: "3",
|
||||
name: "海淀"
|
||||
},
|
||||
{
|
||||
code: "4",
|
||||
name: "朝阳"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
code: "5",
|
||||
name: "北京郊区",
|
||||
children: [
|
||||
{
|
||||
code: "6",
|
||||
name: "海淀郊区"
|
||||
},
|
||||
{
|
||||
code: "7",
|
||||
name: "朝阳郊区"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
code: "11",
|
||||
name: "深圳",
|
||||
children: [
|
||||
{
|
||||
code: "12",
|
||||
name: "深圳市区",
|
||||
children: [
|
||||
{
|
||||
code: "13",
|
||||
name: "南山"
|
||||
},
|
||||
{
|
||||
code: "14",
|
||||
name: "福田"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
code: "15",
|
||||
name: "深圳郊区",
|
||||
children: [
|
||||
{
|
||||
code: "16",
|
||||
name: "南山郊区"
|
||||
},
|
||||
{
|
||||
code: "17",
|
||||
name: "福田郊区"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,87 @@
|
||||
import _ from "lodash-es";
|
||||
export async function getPcasData() {
|
||||
const pcasData = () => import("china-division/dist/pcas-code.json");
|
||||
const ret = await pcasData();
|
||||
return ret.default;
|
||||
}
|
||||
export async function getPcaData() {
|
||||
const pcaData = () => import("china-division/dist/pca-code.json");
|
||||
const ret = await pcaData();
|
||||
return ret.default;
|
||||
}
|
||||
export const TreeNodesLazyLoader = {
|
||||
getNodesByValues(values) {
|
||||
console.log("getNodesByValues", values);
|
||||
if (!(values instanceof Array)) {
|
||||
values = [values];
|
||||
}
|
||||
return getPcasData().then((data) => {
|
||||
const nodes = [];
|
||||
for (const value of values) {
|
||||
const found = this.getNode(data, value);
|
||||
if (found) {
|
||||
const target = _.cloneDeep(found);
|
||||
delete target.children;
|
||||
nodes.push(target);
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
});
|
||||
},
|
||||
getNode(list, value) {
|
||||
for (const item of list) {
|
||||
if (item.code === value) {
|
||||
return item;
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
const found = this.getNode(item.children, value);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getChildren(parent) {
|
||||
return getPcasData().then((data) => {
|
||||
const list = this.getChildrenByParent(parent, data);
|
||||
if (list == null) {
|
||||
return [];
|
||||
}
|
||||
return this.cloneAndDeleteChildren(list);
|
||||
});
|
||||
},
|
||||
getChildrenByParent(parentId, tree) {
|
||||
if (!parentId) {
|
||||
// 取第一级
|
||||
return tree;
|
||||
} else {
|
||||
for (const node of tree) {
|
||||
if (node.code === parentId) {
|
||||
return node.children;
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
// 递归查找
|
||||
const list = this.getChildrenByParent(parentId, node.children);
|
||||
if (list) {
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
cloneAndDeleteChildren(list) {
|
||||
const newList = [];
|
||||
for (const node of list) {
|
||||
const newNode = {};
|
||||
Object.assign(newNode, node);
|
||||
if (newNode.children == null || newNode.children.length === 0) {
|
||||
newNode.isLeaf = true;
|
||||
newNode.leaf = true;
|
||||
}
|
||||
delete newNode.children;
|
||||
newList.push(newNode);
|
||||
}
|
||||
console.log("found children:", newList);
|
||||
return newList;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { mock } from "../api/service";
|
||||
import * as tools from "../api/tools";
|
||||
import _ from "lodash-es";
|
||||
const commonMocks = import.meta.globEager("./common/mock.*.js");
|
||||
const apiMocks = import.meta.globEager("../api/modules/*.mock.ts");
|
||||
const viewMocks = import.meta.globEager("../views/**/mock.js");
|
||||
|
||||
const list = [];
|
||||
_.forEach(commonMocks, (value) => {
|
||||
list.push(value.default);
|
||||
});
|
||||
_.forEach(apiMocks, (value) => {
|
||||
list.push(value.default);
|
||||
});
|
||||
_.forEach(viewMocks, (value) => {
|
||||
list.push(value.default);
|
||||
});
|
||||
|
||||
list.forEach((apiFile) => {
|
||||
for (const item of apiFile) {
|
||||
mock.onAny(new RegExp(item.path)).reply(async (config) => {
|
||||
console.log("------------fake request start -------------");
|
||||
console.log("request:", config);
|
||||
const data = config.data ? JSON.parse(config.data) : {};
|
||||
const query = config.url.indexOf("?") >= 0 ? config.url.substring(config.url.indexOf("?") + 1) : undefined;
|
||||
const params = config.params || {};
|
||||
if (query) {
|
||||
const arr = query.split("&");
|
||||
for (const item of arr) {
|
||||
const kv = item.split("=");
|
||||
params[kv[0]] = kv[1];
|
||||
}
|
||||
}
|
||||
|
||||
const req = {
|
||||
body: data,
|
||||
params: params
|
||||
};
|
||||
const ret = await item.handle(req);
|
||||
console.log("response:", ret);
|
||||
console.log("------------fake request end-------------");
|
||||
if (ret.code === 0) {
|
||||
return tools.responseSuccess(ret.data, ret.msg);
|
||||
} else {
|
||||
return tools.responseError(ret.data, ret.msg, ret.code);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,225 @@
|
||||
import { request, requestForMock } from "/src/api/service";
|
||||
import "/src/mock";
|
||||
import UiAntdv from "@fast-crud/ui-antdv";
|
||||
import { FastCrud, UseCrudProps, useTypes, setLogger } from "@fast-crud/fast-crud";
|
||||
import "@fast-crud/fast-crud/dist/style.css";
|
||||
import { FsExtendsUploader, FsExtendsEditor, FsExtendsJson, FsExtendsCopyable, FsExtendsTime } from "@fast-crud/fast-extends";
|
||||
import "@fast-crud/fast-extends/dist/style.css";
|
||||
|
||||
import { useCrudPermission } from "../permission";
|
||||
|
||||
function install(app, options: any = {}) {
|
||||
app.use(UiAntdv);
|
||||
//设置日志级别
|
||||
setLogger({ level: "debug" });
|
||||
app.use(FastCrud, {
|
||||
i18n: options.i18n,
|
||||
async dictRequest({ url }) {
|
||||
if (url && url.startsWith("/mock")) {
|
||||
//如果是crud开头的dict请求视为mock
|
||||
return await requestForMock({ url, method: "post" });
|
||||
}
|
||||
return await request({ url, method: "post" });
|
||||
},
|
||||
/**
|
||||
* useCrud时会被执行
|
||||
* @param context,useCrud的参数
|
||||
*/
|
||||
commonOptions(context: UseCrudProps) {
|
||||
const crudBinding = context.expose?.crudBinding;
|
||||
const opts = {
|
||||
table: {
|
||||
size: "small",
|
||||
pagination: false,
|
||||
onResizeColumn: (w, col) => {
|
||||
crudBinding.value.table.columnsMap[col.key].width = w;
|
||||
}
|
||||
},
|
||||
rowHandle: {
|
||||
buttons: {
|
||||
view: { type: "link", text: null, icon: "ion:eye-outline" },
|
||||
edit: { type: "link", text: null, icon: "ion:create-outline" },
|
||||
remove: { type: "link", style: { color: "red" }, text: null, icon: "ion:trash-outline" }
|
||||
},
|
||||
dropdown: {
|
||||
more: {
|
||||
type: "link"
|
||||
}
|
||||
}
|
||||
},
|
||||
request: {
|
||||
transformQuery: ({ page, form, sort }) => {
|
||||
const limit = page.pageSize;
|
||||
const currentPage = page.currentPage ?? 1;
|
||||
const offset = limit * (currentPage - 1);
|
||||
|
||||
sort = sort == null ? {} : sort;
|
||||
|
||||
return {
|
||||
page: {
|
||||
limit,
|
||||
offset
|
||||
},
|
||||
query: form,
|
||||
sort
|
||||
};
|
||||
},
|
||||
transformRes: ({ res }) => {
|
||||
const pageSize = res.limit;
|
||||
let currentPage = res.offset / pageSize;
|
||||
if (res.offset % pageSize === 0) {
|
||||
currentPage++;
|
||||
}
|
||||
return { currentPage, pageSize, ...res };
|
||||
}
|
||||
},
|
||||
form: {
|
||||
display: "flex",
|
||||
labelCol: {
|
||||
//固定label宽度
|
||||
span: null,
|
||||
style: {
|
||||
width: "120px"
|
||||
}
|
||||
},
|
||||
wrapperCol: {
|
||||
span: null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 从 useCrud({permission}) 里获取permission参数,去设置各个按钮的权限
|
||||
const crudPermission = useCrudPermission({ permission: context.permission });
|
||||
return crudPermission.merge(opts);
|
||||
}
|
||||
});
|
||||
|
||||
// fast-extends里面的扩展组件均为异步组件,只有在使用时才会被加载,并不会影响首页加载速度
|
||||
//安装uploader 公共参数
|
||||
app.use(FsExtendsUploader, {
|
||||
defaultType: "cos",
|
||||
cos: {
|
||||
domain: "https://d2p-demo-1251260344.cos.ap-guangzhou.myqcloud.com",
|
||||
bucket: "d2p-demo-1251260344",
|
||||
region: "ap-guangzhou",
|
||||
secretId: "", //
|
||||
secretKey: "", // 传了secretKey 和secretId 代表使用本地签名模式(不安全,生产环境不推荐)
|
||||
getAuthorization(custom) {
|
||||
// 不传secretKey代表使用临时签名模式,此时此参数必传(安全,生产环境推荐)
|
||||
return request({
|
||||
url: "http://www.docmirror.cn:7070/api/upload/cos/getAuthorization",
|
||||
method: "get"
|
||||
}).then((ret) => {
|
||||
// 返回结构如下
|
||||
// ret.data:{
|
||||
// TmpSecretId,
|
||||
// TmpSecretKey,
|
||||
// XCosSecurityToken,
|
||||
// ExpiredTime, // SDK 在 ExpiredTime 时间前,不会再次调用 getAuthorization
|
||||
// }
|
||||
return ret;
|
||||
});
|
||||
},
|
||||
successHandle(ret) {
|
||||
// 上传完成后可以在此处处理结果,修改url什么的
|
||||
console.log("success handle:", ret);
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
alioss: {
|
||||
domain: "https://d2p-demo.oss-cn-shenzhen.aliyuncs.com",
|
||||
bucket: "d2p-demo",
|
||||
region: "oss-cn-shenzhen",
|
||||
accessKeyId: "",
|
||||
accessKeySecret: "",
|
||||
async getAuthorization(custom, context) {
|
||||
// 不传accessKeySecret代表使用临时签名模式,此时此参数必传(安全,生产环境推荐)
|
||||
const ret = await request({
|
||||
url: "http://www.docmirror.cn:7070/api/upload/alioss/getAuthorization",
|
||||
method: "get"
|
||||
});
|
||||
console.log("ret", ret);
|
||||
return ret;
|
||||
},
|
||||
sdkOpts: {
|
||||
// sdk配置
|
||||
secure: true // 默认为非https上传,为了安全,设置为true
|
||||
},
|
||||
successHandle(ret) {
|
||||
// 上传完成后可以在此处处理结果,修改url什么的
|
||||
console.log("success handle:", ret);
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
qiniu: {
|
||||
bucket: "d2p-demo",
|
||||
async getToken(options) {
|
||||
const ret = await request({
|
||||
url: "http://www.docmirror.cn:7070/api/upload/qiniu/getToken",
|
||||
method: "get"
|
||||
});
|
||||
return ret; // {token:xxx,expires:xxx}
|
||||
},
|
||||
successHandle(ret) {
|
||||
// 上传完成后可以在此处处理结果,修改url什么的
|
||||
console.log("success handle:", ret);
|
||||
return ret;
|
||||
},
|
||||
domain: "http://d2p.file.handsfree.work/"
|
||||
},
|
||||
form: {
|
||||
action: "http://www.docmirror.cn:7070/api/upload/form/upload",
|
||||
name: "file",
|
||||
withCredentials: false,
|
||||
uploadRequest: async ({ action, file, onProgress }) => {
|
||||
// @ts-ignore
|
||||
const data = new FormData();
|
||||
data.append("file", file);
|
||||
return await request({
|
||||
url: action,
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
},
|
||||
timeout: 60000,
|
||||
data,
|
||||
onUploadProgress: (p) => {
|
||||
onProgress({ percent: Math.round((p.loaded / p.total) * 100) });
|
||||
}
|
||||
});
|
||||
},
|
||||
successHandle(ret) {
|
||||
// 上传完成后的结果处理, 此处应返回格式为{url:xxx}
|
||||
return {
|
||||
url: "http://www.docmirror.cn:7070" + ret,
|
||||
key: ret.replace("/api/upload/form/download?key=", "")
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//安装editor
|
||||
app.use(FsExtendsEditor, {
|
||||
//编辑器的公共配置
|
||||
wangEditor: {}
|
||||
});
|
||||
app.use(FsExtendsJson);
|
||||
app.use(FsExtendsTime);
|
||||
app.use(FsExtendsCopyable);
|
||||
|
||||
const { addTypes } = useTypes();
|
||||
addTypes({
|
||||
time2: {
|
||||
//如果与官方字段类型同名,将会覆盖官方的字段类型
|
||||
form: { component: { name: "a-date-picker" } },
|
||||
column: { component: { name: "fs-date-format", format: "YYYY-MM-DD" } },
|
||||
valueBuilder(context) {
|
||||
console.log("time2,valueBuilder", context);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
install
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import "./iconfont.js"
|
||||
@@ -0,0 +1,2 @@
|
||||
// import "@iconify/iconify";
|
||||
import "@purge-icons/generated";
|
||||
@@ -0,0 +1,12 @@
|
||||
import "./iconify";
|
||||
import "./iconfont";
|
||||
import FastCrud from "./fast-crud";
|
||||
import permission from "./permission";
|
||||
function install(app, options: any = {}) {
|
||||
app.use(FastCrud, options);
|
||||
app.use(permission);
|
||||
}
|
||||
|
||||
export default {
|
||||
install
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { request } from "/src/api/service";
|
||||
export async function getPermissions() {
|
||||
const ret = await request({
|
||||
url: "/sys/authority/user/permissions",
|
||||
method: "post"
|
||||
});
|
||||
// 如果使用你自己的后端,需要在此处将返回结果改造为本模块需要的结构
|
||||
// 结构详情,请参考示例中打印的日志 ”获取权限数据成功:{...}“ (实际上就是“资源管理”页面中列出来的数据)
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import permission from "./permission";
|
||||
import permissionUtil from "../util.permission";
|
||||
const install = function (app) {
|
||||
app.directive("permission", permission);
|
||||
app.config.globalProperties.$hasPermissions = permissionUtil.hasPermissions;
|
||||
};
|
||||
|
||||
permission.install = install;
|
||||
export default permission;
|
||||
@@ -0,0 +1,11 @@
|
||||
import permissionUtil from "../util.permission";
|
||||
export default {
|
||||
mounted(el, binding, vnode) {
|
||||
const { value } = binding;
|
||||
const hasPermission = permissionUtil.hasPermissions(value);
|
||||
|
||||
if (!hasPermission) {
|
||||
el.parentNode && el.parentNode.removeChild(el);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export class NoPermissionError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message || "对不起,您没有权限执行此操作");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import router from "/src/router";
|
||||
import { useUserStore } from "/@/store/modules/user";
|
||||
import { usePermissionStore } from "./store.permission";
|
||||
import util from "./util.permission";
|
||||
import { message } from "ant-design-vue";
|
||||
import NProgress from "nprogress";
|
||||
export function registerRouterHook() {
|
||||
// 注册路由beforeEach钩子,在第一次加载路由页面时,加载权限
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const permissionStore = usePermissionStore();
|
||||
if (permissionStore.isInited) {
|
||||
if (to.meta.permission) {
|
||||
//校验权限
|
||||
// @ts-ignore
|
||||
if (!util.hasPermissions(to.meta.permission)) {
|
||||
//没有权限
|
||||
message.warn("对不起,您没有权限");
|
||||
//throw new Error("对不起,您没有权限");
|
||||
NProgress.done();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
const token = userStore.getToken;
|
||||
if (!token || token === "undefined") {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化权限列表
|
||||
try {
|
||||
console.log("permission is enabled");
|
||||
await permissionStore.loadFromRemote();
|
||||
console.log("PM load success");
|
||||
next({ ...to, replace: true });
|
||||
} catch (e) {
|
||||
console.error("加载动态路由失败", e);
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import permissionDirective from "./directive/index";
|
||||
import { registerRouterHook } from "./hook";
|
||||
import util from "./util.permission";
|
||||
export * from "./use-crud-permission";
|
||||
export * from "./errors";
|
||||
|
||||
export function usePermission() {
|
||||
return {
|
||||
...util
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
install(app) {
|
||||
// 开启权限模块
|
||||
// 注册v-permission指令, 用于控制按钮权限
|
||||
app.use(permissionDirective);
|
||||
// 注册路由钩子
|
||||
// 通过路由守卫,在登录成功后拦截路由,从后台加载权限数据
|
||||
// 然后将权限数据转化为菜单和路由,添加到系统中
|
||||
registerRouterHook();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { useResourceStore } from "/src/store/modules/resource";
|
||||
import { getPermissions } from "./api";
|
||||
import { mitter } from "/@/utils/util.mitt";
|
||||
import { env } from "/@/utils/util.env";
|
||||
|
||||
//监听注销事件
|
||||
mitter.on("app.logout", () => {
|
||||
const permissionStore = usePermissionStore();
|
||||
permissionStore.clear();
|
||||
});
|
||||
|
||||
interface PermissionState {
|
||||
permissions: [];
|
||||
inited: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建权限码列表
|
||||
* @param menuTree
|
||||
* @param permissionList
|
||||
* @returns {*}
|
||||
*/
|
||||
function formatPermissions(menuTree: Array<any>, permissionList = []) {
|
||||
if (menuTree == null) {
|
||||
menuTree = [];
|
||||
}
|
||||
menuTree.forEach((item: any) => {
|
||||
if (item.permission) {
|
||||
// @ts-ignore
|
||||
permissionList.push(item.permission);
|
||||
}
|
||||
if (item.children != null && item.children.length > 0) {
|
||||
formatPermissions(item.children, permissionList);
|
||||
}
|
||||
});
|
||||
return permissionList;
|
||||
}
|
||||
|
||||
export const usePermissionStore = defineStore({
|
||||
id: "app.permission",
|
||||
state: (): PermissionState => ({
|
||||
permissions: [],
|
||||
inited: false
|
||||
}),
|
||||
getters: {
|
||||
getPermissions() {
|
||||
// @ts-ignore
|
||||
return this.permissions;
|
||||
},
|
||||
isInited() {
|
||||
// @ts-ignore
|
||||
return this.inited;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
init({ permissions }) {
|
||||
this.permissions = permissions;
|
||||
this.inited = true;
|
||||
},
|
||||
clear() {
|
||||
this.permissions = [];
|
||||
this.inited = false;
|
||||
},
|
||||
resolve(resourceTree) {
|
||||
const permissions = formatPermissions(resourceTree);
|
||||
this.init({ permissions });
|
||||
|
||||
//过滤没有权限的菜单
|
||||
const resourceStore = useResourceStore();
|
||||
resourceStore.filterByPermission(permissions);
|
||||
},
|
||||
async loadFromRemote() {
|
||||
let permissionTree = [];
|
||||
if (env.PM_ENABLED === "false") {
|
||||
console.warn("当前权限模块未开启,权限列表为空");
|
||||
} else {
|
||||
//开启了权限模块,向后台请求权限列表
|
||||
const data = await getPermissions();
|
||||
if (data != null) {
|
||||
permissionTree = data;
|
||||
} else {
|
||||
console.warn("当前获取到的权限列表为空");
|
||||
}
|
||||
}
|
||||
this.resolve(permissionTree);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { usePermission } from "/@/plugin/permission";
|
||||
import _ from "lodash-es";
|
||||
|
||||
/**
|
||||
* 设置按钮动作权限
|
||||
* @param permission {prefix,extra}
|
||||
*/
|
||||
export function useCrudPermission({ permission }) {
|
||||
const { hasPermissions } = usePermission();
|
||||
|
||||
const prefix = permission instanceof Object ? permission.prefix : permission;
|
||||
|
||||
//根据权限显示按钮
|
||||
function hasActionPermission(action) {
|
||||
if (!prefix) {
|
||||
return true;
|
||||
}
|
||||
return hasPermissions(prefix + ":" + action);
|
||||
}
|
||||
|
||||
function buildCrudPermission() {
|
||||
if (permission == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let extra = {};
|
||||
if (permission instanceof Object) {
|
||||
extra = permission.extra;
|
||||
if (permission.extra && permission.extra instanceof Function) {
|
||||
extra = permission.extra({ hasActionPermission });
|
||||
}
|
||||
}
|
||||
|
||||
return _.merge(
|
||||
{
|
||||
actionbar: {
|
||||
buttons: {
|
||||
add: { show: hasActionPermission("add") }
|
||||
}
|
||||
},
|
||||
rowHandle: {
|
||||
buttons: {
|
||||
edit: { show: hasActionPermission("edit") },
|
||||
remove: { show: hasActionPermission("remove") },
|
||||
view: { show: hasActionPermission("view") }
|
||||
}
|
||||
}
|
||||
},
|
||||
extra
|
||||
);
|
||||
}
|
||||
|
||||
function merge(userOptions) {
|
||||
const permissionOptions = buildCrudPermission();
|
||||
_.merge(permissionOptions, userOptions);
|
||||
return permissionOptions;
|
||||
}
|
||||
|
||||
return { merge, buildCrudPermission, hasActionPermission };
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { usePermissionStore } from "./store.permission";
|
||||
import { NoPermissionError } from "./errors";
|
||||
import { message } from "ant-design-vue";
|
||||
const util = {
|
||||
hasPermissions: (value: string | string[]): boolean => {
|
||||
let need: string[] = [];
|
||||
if (typeof value === "string") {
|
||||
need.push(value);
|
||||
} else if (value && value instanceof Array && value.length > 0) {
|
||||
need = need.concat(value);
|
||||
}
|
||||
if (need.length === 0) {
|
||||
throw new Error('need permissions! Like "sys:user:view" ');
|
||||
}
|
||||
const permissionStore = usePermissionStore();
|
||||
const userPermissionList = permissionStore.getPermissions;
|
||||
return userPermissionList.some((permission) => {
|
||||
return need.includes(permission);
|
||||
});
|
||||
},
|
||||
requirePermissions: (value) => {
|
||||
if (!util.hasPermissions(value)) {
|
||||
message.error("对不起,您没有权限执行此操作");
|
||||
throw new NoPermissionError();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default util;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
// 进度条
|
||||
import NProgress from "nprogress";
|
||||
import "nprogress/nprogress.css";
|
||||
import { usePageStore } from "../store/modules/page";
|
||||
import { site } from "../utils/util.site";
|
||||
import { routes } from "./resolve";
|
||||
import { useResourceStore } from "../store/modules/resource";
|
||||
import { useUserStore } from "../store/modules/user";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
/**
|
||||
* 路由拦截
|
||||
*/
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// 进度条
|
||||
NProgress.start();
|
||||
|
||||
// 验证当前路由所有的匹配中是否需要有登录验证的
|
||||
if (
|
||||
to.matched.some((r) => {
|
||||
return r.meta?.auth || r.meta?.permission;
|
||||
})
|
||||
) {
|
||||
const userStore = useUserStore();
|
||||
// 这里暂时将cookie里是否存有token作为验证是否登录的条件
|
||||
// 请根据自身业务需要修改
|
||||
const token = userStore.getToken;
|
||||
if (token) {
|
||||
next();
|
||||
} else {
|
||||
// 没有登录的时候跳转到登录界面
|
||||
// 携带上登陆成功之后需要跳转的页面完整路径
|
||||
next({
|
||||
name: "login",
|
||||
query: {
|
||||
redirect: to.fullPath
|
||||
}
|
||||
});
|
||||
// https://github.com/d2-projects/d2-admin/issues/138
|
||||
NProgress.done();
|
||||
}
|
||||
} else {
|
||||
// 不需要身份校验 直接通过
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
// 进度条
|
||||
NProgress.done();
|
||||
// 多页控制 打开新的页面
|
||||
const pageStore = usePageStore();
|
||||
pageStore.open(to);
|
||||
// 更改标题
|
||||
site.title(to.meta.title);
|
||||
|
||||
//修改左侧边栏
|
||||
const matched = to.matched;
|
||||
if (matched.length > 0) {
|
||||
const resourceStore = useResourceStore();
|
||||
resourceStore.setAsideMenuByCurrentRoute(matched);
|
||||
}
|
||||
});
|
||||
export default router;
|
||||
@@ -0,0 +1,155 @@
|
||||
import LayoutPass from "/src/layout/layout-pass.vue";
|
||||
import _ from "lodash-es";
|
||||
import { outsideResource } from "./source/outside";
|
||||
import { headerResource } from "./source/header";
|
||||
import { frameworkResource } from "./source/framework";
|
||||
// @ts-ignore
|
||||
const modules = import.meta.glob("/src/views/**/*.vue");
|
||||
|
||||
let index = 0;
|
||||
function transformOneResource(resource) {
|
||||
let menu: any = null;
|
||||
if (resource.meta == null) {
|
||||
resource.meta = {};
|
||||
}
|
||||
const meta = resource.meta;
|
||||
meta.title = meta.title ?? resource.title ?? "未命名";
|
||||
if (resource.title == null) {
|
||||
resource.title = meta.title;
|
||||
}
|
||||
if (meta.isMenu === false) {
|
||||
menu = null;
|
||||
} else {
|
||||
menu = _.cloneDeep(resource);
|
||||
delete menu.component;
|
||||
}
|
||||
let route;
|
||||
if (resource.type !== "menu") {
|
||||
if (resource.path == null || resource.path.startsWith("https://") || resource.path.startsWith("http://")) {
|
||||
//没有route
|
||||
route = null;
|
||||
} else {
|
||||
route = _.cloneDeep(resource);
|
||||
if (route.component && typeof route.component === "string") {
|
||||
const path = "/src/views" + route.component;
|
||||
route.component = modules[path];
|
||||
}
|
||||
if (route.component == null) {
|
||||
route.component = LayoutPass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
menu,
|
||||
route
|
||||
};
|
||||
}
|
||||
|
||||
export const buildMenusAndRouters = (resources) => {
|
||||
const routes: Array<any> = [];
|
||||
const menus: Array<any> = [];
|
||||
for (const item of resources) {
|
||||
const { menu, route } = transformOneResource(item);
|
||||
let menuChildren;
|
||||
let routeChildren;
|
||||
if (item.children) {
|
||||
if (item.children.length > 0) {
|
||||
const ret = buildMenusAndRouters(item.children);
|
||||
menuChildren = ret.menus;
|
||||
routeChildren = ret.routes;
|
||||
}
|
||||
}
|
||||
|
||||
if (menu) {
|
||||
menus.push(menu);
|
||||
menu.children = menuChildren;
|
||||
}
|
||||
if (route) {
|
||||
if (route?.meta?.cache !== false) {
|
||||
if (route.meta == null) {
|
||||
route.meta = {};
|
||||
}
|
||||
route.meta.cache = true;
|
||||
}
|
||||
routes.push(route);
|
||||
route.children = routeChildren;
|
||||
}
|
||||
}
|
||||
|
||||
setIndex(menus);
|
||||
return {
|
||||
routes,
|
||||
menus
|
||||
};
|
||||
};
|
||||
|
||||
function setIndex(menus) {
|
||||
for (const menu of menus) {
|
||||
menu.index = "index_" + index;
|
||||
index++;
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
setIndex(menu.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findMenus(menus, condition) {
|
||||
const list: any = [];
|
||||
for (const menu of menus) {
|
||||
if (condition(menu)) {
|
||||
list.push(menu);
|
||||
}
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const subList = findMenus(menu.children, condition);
|
||||
for (const item of subList) {
|
||||
list.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function filterMenus(menus, condition) {
|
||||
const list = menus.filter((item) => {
|
||||
return condition(item);
|
||||
});
|
||||
|
||||
for (const item of list) {
|
||||
if (item.children && item.children.length > 0) {
|
||||
item.children = filterMenus(item.children, condition);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function flatChildren(list, children) {
|
||||
for (const child of children) {
|
||||
list.push(child);
|
||||
if (child.children && child.children.length > 0) {
|
||||
flatChildren(list, child.children);
|
||||
}
|
||||
child.children = null;
|
||||
}
|
||||
}
|
||||
function flatSubRouters(routers) {
|
||||
for (const router of routers) {
|
||||
const children: Array<any> = [];
|
||||
if (router.children && router.children.length > 0) {
|
||||
flatChildren(children, router.children);
|
||||
}
|
||||
router.children = children;
|
||||
}
|
||||
return routers;
|
||||
}
|
||||
|
||||
const frameworkRet = buildMenusAndRouters(frameworkResource);
|
||||
const outsideRet = buildMenusAndRouters(outsideResource);
|
||||
const headerRet = buildMenusAndRouters(headerResource);
|
||||
|
||||
const outsideRoutes = outsideRet.routes;
|
||||
const frameworkRoutes = flatSubRouters(frameworkRet.routes);
|
||||
const routes = [...outsideRoutes, ...frameworkRoutes];
|
||||
const frameworkMenus = frameworkRet.menus;
|
||||
const headerMenus = headerRet.menus;
|
||||
export { routes, outsideRoutes, frameworkRoutes, frameworkMenus, headerMenus, findMenus, filterMenus };
|
||||
@@ -0,0 +1,35 @@
|
||||
import LayoutFramework from "/src/layout/layout-framework.vue";
|
||||
//import { crudResources } from "/@/router/source/modules/crud";
|
||||
import { sysResources } from "/@/router/source/modules/sys";
|
||||
import { certdResources } from "/@/router/source/modules/certd";
|
||||
|
||||
export const frameworkResource = [
|
||||
{
|
||||
title: "框架",
|
||||
name: "framework",
|
||||
path: "/",
|
||||
redirect: "/index",
|
||||
component: LayoutFramework,
|
||||
meta: {
|
||||
icon: "ion:accessibility",
|
||||
auth: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "首页",
|
||||
name: "index",
|
||||
path: "/index",
|
||||
component: "/framework/home/index.vue",
|
||||
meta: {
|
||||
fixedAside: true,
|
||||
showOnHeader: false,
|
||||
icon: "ion:home-outline"
|
||||
}
|
||||
},
|
||||
//...crudResources,
|
||||
...certdResources,
|
||||
...sysResources
|
||||
]
|
||||
}
|
||||
];
|
||||
console.assert(frameworkResource.length === 1, "frameworkResource数组长度只能为1,你只能配置framework路由的子路由");
|
||||
@@ -0,0 +1,73 @@
|
||||
export const headerResource = [
|
||||
{
|
||||
title: "文档",
|
||||
path: "http://fast-crud.docmirror.cn/"
|
||||
},
|
||||
{
|
||||
title: "其他Demo",
|
||||
name: "demo",
|
||||
children: [
|
||||
{
|
||||
title: "Element版",
|
||||
path: "http://fast-crud.docmirror.cn/element/"
|
||||
},
|
||||
{
|
||||
title: "VbenAdmin",
|
||||
path: "http://fast-crud.docmirror.cn/vben/"
|
||||
},
|
||||
{
|
||||
title: "cool-admin-vue",
|
||||
path: "http://fast-crud.docmirror.cn/cool/"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
title: "源码",
|
||||
name: "source",
|
||||
key: "source",
|
||||
meta: {
|
||||
icon: "ion:git-branch-outline"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "fast-crud",
|
||||
children: [
|
||||
{
|
||||
title: "github",
|
||||
path: "http://github.com/fast-crud/fast-crud",
|
||||
meta: {
|
||||
icon: "ion:logo-github"
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "gitee",
|
||||
path: "http://gitee.com/fast-crud/fast-crud",
|
||||
meta: {
|
||||
icon: "ion:logo-octocat"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "fs-admin",
|
||||
children: [
|
||||
{
|
||||
title: "github",
|
||||
path: "http://github.com/fast-crud/fs-admin-antdv",
|
||||
meta: {
|
||||
icon: "ion:logo-github"
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "gitee",
|
||||
path: "http://gitee.com/fast-crud/fs-admin-antdv",
|
||||
meta: {
|
||||
icon: "ion:logo-octocat"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,41 @@
|
||||
export const certdResources = [
|
||||
{
|
||||
title: "证书自动化",
|
||||
name: "certd",
|
||||
path: "/certd",
|
||||
redirect: "/certd/pipeline",
|
||||
meta: {
|
||||
icon: "ion:key-outline",
|
||||
auth: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "证书自动化流水线",
|
||||
name: "pipeline",
|
||||
path: "/certd/pipeline",
|
||||
component: "/certd/pipeline/index.vue",
|
||||
meta: {
|
||||
icon: "ion:analytics-sharp"
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "编辑流水线",
|
||||
name: "pipelineEdit",
|
||||
path: "/certd/pipeline/detail",
|
||||
component: "/certd/pipeline/detail.vue",
|
||||
meta: {
|
||||
isMenu: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "授权管理",
|
||||
name: "access",
|
||||
path: "/certd/access",
|
||||
component: "/certd/access/index.vue",
|
||||
meta: {
|
||||
icon: "ion:disc-outline"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,604 @@
|
||||
export const crudResources = [
|
||||
{
|
||||
title: "CRUD示例",
|
||||
name: "crud",
|
||||
path: "/crud",
|
||||
redirect: "/crud/basis",
|
||||
meta: {
|
||||
icon: "ion:apps-sharp"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "基本特性",
|
||||
name: "basis",
|
||||
path: "/crud/basis",
|
||||
redirect: "/crud/basis/i18n",
|
||||
meta: {
|
||||
icon: "ion:disc-outline"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "HelloWorld",
|
||||
name: "FsCrudFirst",
|
||||
path: "/crud/basis/first",
|
||||
component: "/crud/basis/first/index.vue"
|
||||
},
|
||||
{
|
||||
title: "动态计算",
|
||||
name: "BasisCompute",
|
||||
path: "/crud/basis/compute",
|
||||
component: "/crud/basis/compute/index.vue"
|
||||
},
|
||||
{
|
||||
title: "动态计算-更多示例",
|
||||
name: "BasisComputeMore",
|
||||
path: "/crud/basis/compute-more",
|
||||
component: "/crud/basis/compute-more/index.vue"
|
||||
},
|
||||
{
|
||||
title: "国际化",
|
||||
name: "BasisI18n",
|
||||
path: "/crud/basis/i18n",
|
||||
component: "/crud/basis/i18n/index.vue"
|
||||
},
|
||||
{
|
||||
title: "ValueChange",
|
||||
name: "BasisValueChange",
|
||||
path: "/crud/basis/value-change",
|
||||
component: "/crud/basis/value-change/index.vue"
|
||||
},
|
||||
{
|
||||
title: "Card布局",
|
||||
name: "BasisLayoutCard",
|
||||
path: "/crud/basis/layout-card",
|
||||
component: "/crud/basis/layout-card/index.vue"
|
||||
},
|
||||
{
|
||||
title: "自定义布局",
|
||||
name: "BasisLayoutCustom",
|
||||
path: "/crud/basis/layout-custom",
|
||||
component: "/crud/basis/layout-custom/index.vue"
|
||||
},
|
||||
{
|
||||
title: "列设置",
|
||||
name: "BasisColumnsSet",
|
||||
path: "/crud/basis/columns-set",
|
||||
component: "/crud/basis/columns-set/index.vue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "数据字典",
|
||||
name: "dict",
|
||||
path: "/crud/dict",
|
||||
redirect: "/crud/dict/single",
|
||||
meta: {
|
||||
icon: "ion:book-outline"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "单例",
|
||||
name: "DictSingle",
|
||||
path: "/crud/dict/single",
|
||||
component: "/crud/dict/single/index.vue"
|
||||
},
|
||||
{
|
||||
title: "分发复制",
|
||||
name: "DictCloneable",
|
||||
path: "/crud/dict/cloneable",
|
||||
component: "/crud/dict/cloneable/index.vue"
|
||||
},
|
||||
{
|
||||
title: "原型复制",
|
||||
name: "DictPrototype",
|
||||
path: "/crud/dict/prototype",
|
||||
component: "/crud/dict/prototype/index.vue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "操作列",
|
||||
name: "row-handle",
|
||||
path: "/crud/row-handle",
|
||||
redirect: "/crud/row-handle/tooltip",
|
||||
meta: {
|
||||
icon: "ion:build-outline"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "Tooltip",
|
||||
name: "RowHandleTooltip",
|
||||
path: "/crud/row-handle/tooltip",
|
||||
component: "/crud/row-handle/tooltip/index.vue"
|
||||
},
|
||||
{
|
||||
title: "按钮折叠",
|
||||
name: "RowHandleDropdown",
|
||||
path: "/crud/row-handle/dropdown",
|
||||
component: "/crud/row-handle/dropdown/index.vue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "组件示例",
|
||||
name: "component",
|
||||
path: "/crud/component",
|
||||
redirect: "/crud/component/text",
|
||||
meta: {
|
||||
icon: "ion:cube-outline"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "文本输入(input)",
|
||||
name: "ComponentText",
|
||||
path: "/crud/component/text",
|
||||
component: "/crud/component/text/index.vue"
|
||||
},
|
||||
{
|
||||
title: "选择(select)",
|
||||
name: "ComponentSelect",
|
||||
path: "/crud/component/select",
|
||||
component: "/crud/component/select/index.vue"
|
||||
},
|
||||
{
|
||||
title: "级联(cascader)",
|
||||
name: "ComponentCascader",
|
||||
path: "/crud/component/cascader",
|
||||
component: "/crud/component/cascader/index.vue"
|
||||
},
|
||||
{
|
||||
title: "多选(checkbox)",
|
||||
name: "ComponentCheckbox",
|
||||
path: "/crud/component/checkbox",
|
||||
component: "/crud/component/checkbox/index.vue"
|
||||
},
|
||||
{
|
||||
title: "单选(radio)",
|
||||
name: "ComponentRadio",
|
||||
path: "/crud/component/radio",
|
||||
component: "/crud/component/radio/index.vue"
|
||||
},
|
||||
{
|
||||
title: "开关(switch)",
|
||||
name: "ComponentSwitch",
|
||||
path: "/crud/component/switch",
|
||||
component: "/crud/component/switch/index.vue"
|
||||
},
|
||||
{
|
||||
title: "日期时间(date)",
|
||||
name: "ComponentDate",
|
||||
path: "/crud/component/date",
|
||||
component: "/crud/component/date/index.vue"
|
||||
},
|
||||
{
|
||||
title: "按钮链接",
|
||||
name: "ComponentButton",
|
||||
path: "/crud/component/button",
|
||||
component: "/crud/component/button/index.vue"
|
||||
},
|
||||
{
|
||||
title: "数字",
|
||||
name: "ComponentNumber",
|
||||
path: "/crud/component/number",
|
||||
component: "/crud/component/number/index.vue"
|
||||
},
|
||||
{
|
||||
title: "树形选择",
|
||||
name: "ComponentTree",
|
||||
path: "/crud/component/tree",
|
||||
component: "/crud/component/tree/index.vue"
|
||||
},
|
||||
{
|
||||
title: "图片裁剪上传",
|
||||
name: "ComponentUploaderCropper",
|
||||
path: "/crud/component/uploader/cropper",
|
||||
component: "/crud/component/uploader/cropper/index.vue"
|
||||
},
|
||||
{
|
||||
title: "表单本地上传",
|
||||
name: "ComponentUploaderForm",
|
||||
path: "/crud/component/uploader/form",
|
||||
component: "/crud/component/uploader/form/index.vue"
|
||||
},
|
||||
{
|
||||
title: "阿里云oss上传",
|
||||
name: "ComponentUploaderAlioss",
|
||||
path: "/crud/component/uploader/alioss",
|
||||
component: "/crud/component/uploader/alioss/index.vue"
|
||||
},
|
||||
{
|
||||
title: "腾讯云cos上传",
|
||||
name: "ComponentUploaderCos",
|
||||
path: "/crud/component/uploader/cos",
|
||||
component: "/crud/component/uploader/cos/index.vue"
|
||||
},
|
||||
{
|
||||
title: "七牛云上传",
|
||||
name: "ComponentUploaderQiniu",
|
||||
path: "/crud/component/uploader/qiniu",
|
||||
component: "/crud/component/uploader/qiniu/index.vue"
|
||||
},
|
||||
{
|
||||
title: "富文本编辑器",
|
||||
name: "ComponentEditor",
|
||||
path: "/crud/component/editor",
|
||||
component: "/crud/component/editor/index.vue"
|
||||
},
|
||||
{
|
||||
title: "图标",
|
||||
name: "ComponentIcon",
|
||||
path: "/crud/component/icon",
|
||||
component: "/crud/component/icon/index.vue"
|
||||
},
|
||||
{
|
||||
title: "JsonEditor",
|
||||
name: "ComponentJson",
|
||||
path: "/crud/component/json",
|
||||
component: "/crud/component/json/index.vue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Form表单",
|
||||
name: "form",
|
||||
path: "/crud/form",
|
||||
redirect: "/crud/form/layout",
|
||||
meta: {
|
||||
icon: "ion:document-text-outline"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "基本表单",
|
||||
name: "FormBase",
|
||||
path: "/crud/form/base",
|
||||
component: "/crud/form/base/index.vue"
|
||||
},
|
||||
{
|
||||
title: "表单Grid布局",
|
||||
name: "FormLayoutGrid",
|
||||
path: "/crud/form/layout-grid",
|
||||
component: "/crud/form/layout-grid/index.vue"
|
||||
},
|
||||
{
|
||||
title: "表单Flex布局",
|
||||
name: "FormLayoutFlex",
|
||||
path: "/crud/form/layout-flex",
|
||||
component: "/crud/form/layout-flex/index.vue"
|
||||
},
|
||||
{
|
||||
title: "表单动态布局",
|
||||
name: "FormLayout",
|
||||
path: "/crud/form/layout",
|
||||
component: "/crud/form/layout/index.vue"
|
||||
},
|
||||
{
|
||||
title: "表单校验",
|
||||
name: "FormValidation",
|
||||
path: "/crud/form/validation",
|
||||
component: "/crud/form/validation/index.vue"
|
||||
},
|
||||
{
|
||||
title: "抽屉表单",
|
||||
name: "FormDrawer",
|
||||
path: "/crud/form/drawer",
|
||||
component: "/crud/form/drawer/index.vue"
|
||||
},
|
||||
{
|
||||
title: "表单分组",
|
||||
name: "FormGroup",
|
||||
path: "/crud/form/group",
|
||||
component: "/crud/form/group/index.vue"
|
||||
},
|
||||
{
|
||||
title: "表单分组(tabs)",
|
||||
name: "FormGroupTabs",
|
||||
path: "/crud/form/group-tabs",
|
||||
component: "/crud/form/group-tabs/index.vue"
|
||||
},
|
||||
{
|
||||
title: "自定义表单",
|
||||
name: "FormCustomForm",
|
||||
path: "/crud/form/custom-form",
|
||||
component: "/crud/form/custom-form/index.vue"
|
||||
},
|
||||
{
|
||||
title: "字段帮助说明",
|
||||
name: "FormHelper",
|
||||
path: "/crud/form/helper",
|
||||
component: "/crud/form/helper/index.vue"
|
||||
},
|
||||
{
|
||||
title: "页面内部弹出表单",
|
||||
name: "FormInner",
|
||||
path: "/crud/form/inner",
|
||||
component: "/crud/form/inner/index.vue",
|
||||
meta: {
|
||||
cache: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "地区字典管理",
|
||||
name: "FormInnerArea",
|
||||
path: "/crud/form/inner/area",
|
||||
component: "/crud/form/inner/area/index.vue",
|
||||
meta: {
|
||||
isMenu: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "新页面编辑",
|
||||
name: "FormNewPage",
|
||||
path: "/crud/form/new-page",
|
||||
component: "/crud/form/new-page/index.vue",
|
||||
meta: {
|
||||
cache: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "新页面编辑表单",
|
||||
name: "FormNewPageEdit",
|
||||
path: "/crud/form/new-page/edit",
|
||||
component: "/crud/form/new-page/edit.vue",
|
||||
meta: {
|
||||
isMenu: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "独立使用表单",
|
||||
name: "FormIndependent",
|
||||
path: "/crud/form/independent",
|
||||
component: "/crud/form/independent/index.vue"
|
||||
},
|
||||
{
|
||||
title: "重置表单",
|
||||
name: "FormReset",
|
||||
path: "/crud/form/reset",
|
||||
component: "/crud/form/reset/index.vue"
|
||||
},
|
||||
{
|
||||
title: "嵌套数据结构",
|
||||
name: "FormNest",
|
||||
path: "/crud/form/nest",
|
||||
component: "/crud/form/nest/index.vue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "表格特性",
|
||||
path: "/crud/feature",
|
||||
meta: {
|
||||
icon: "ion:beer-outline"
|
||||
},
|
||||
redirect: "/crud/feature/dropdown",
|
||||
children: [
|
||||
{
|
||||
title: "部件显隐",
|
||||
name: "FeatureHide",
|
||||
path: "/crud/feature/hide",
|
||||
component: "/crud/feature/hide/index.vue"
|
||||
},
|
||||
{
|
||||
title: "多选&批量删除",
|
||||
name: "FeatureSelection",
|
||||
path: "/crud/feature/selection",
|
||||
component: "/crud/feature/selection/index.vue"
|
||||
},
|
||||
{
|
||||
title: "单选",
|
||||
name: "FeatureSelectionRadio",
|
||||
path: "/crud/feature/selection-radio",
|
||||
component: "/crud/feature/selection-radio/index.vue"
|
||||
},
|
||||
{
|
||||
title: "表头过滤",
|
||||
name: "FeatureFilter",
|
||||
path: "/crud/feature/filter",
|
||||
component: "/crud/feature/filter/index.vue"
|
||||
},
|
||||
{
|
||||
title: "行展开",
|
||||
name: "FeatureExpand",
|
||||
path: "/crud/feature/expand",
|
||||
component: "/crud/feature/expand/index.vue"
|
||||
},
|
||||
{
|
||||
title: "树形表格",
|
||||
name: "FeatureTree",
|
||||
path: "/crud/feature/tree",
|
||||
component: "/crud/feature/tree/index.vue"
|
||||
},
|
||||
{
|
||||
title: "多级表头",
|
||||
name: "FeatureHeaderGroup",
|
||||
path: "/crud/feature/header-group",
|
||||
component: "/crud/feature/header-group/index.vue"
|
||||
},
|
||||
{
|
||||
title: "合并单元格",
|
||||
name: "FeatureMerge",
|
||||
path: "/crud/feature/merge",
|
||||
component: "/crud/feature/merge/index.vue"
|
||||
},
|
||||
{
|
||||
title: "序号",
|
||||
name: "FeatureIndex",
|
||||
path: "/crud/feature/index",
|
||||
component: "/crud/feature/index/index.vue"
|
||||
},
|
||||
{
|
||||
title: "排序",
|
||||
name: "FeatureSortable",
|
||||
path: "/crud/feature/sortable",
|
||||
component: "/crud/feature/sortable/index.vue"
|
||||
},
|
||||
{
|
||||
title: "固定列",
|
||||
name: "FeatureFixed",
|
||||
path: "/crud/feature/fixed",
|
||||
component: "/crud/feature/fixed/index.vue"
|
||||
},
|
||||
{
|
||||
title: "不固定高度",
|
||||
name: "FeatureHeight",
|
||||
path: "/crud/feature/height",
|
||||
component: "/crud/feature/height/index.vue"
|
||||
},
|
||||
{
|
||||
title: "可编辑",
|
||||
name: "FeatureEditable",
|
||||
path: "/crud/feature/editable",
|
||||
component: "/crud/feature/editable/index.vue"
|
||||
},
|
||||
{
|
||||
title: "行编辑",
|
||||
name: "FeatureEditableRow",
|
||||
path: "/crud/feature/editable-row",
|
||||
component: "/crud/feature/editable-row/index.vue"
|
||||
},
|
||||
{
|
||||
title: "查询框",
|
||||
name: "FeatureSearch",
|
||||
path: "/crud/feature/search",
|
||||
component: "/crud/feature/search/index.vue"
|
||||
},
|
||||
{
|
||||
title: "查询框多行模式",
|
||||
name: "FeatureSearchMulti",
|
||||
path: "/crud/feature/search-multi",
|
||||
component: "/crud/feature/search-multi/index.vue"
|
||||
},
|
||||
{
|
||||
title: "字段排序",
|
||||
name: "FeatureColumnSort",
|
||||
path: "/crud/feature/column-sort",
|
||||
component: "/crud/feature/column-sort/index.vue"
|
||||
},
|
||||
{
|
||||
title: "ValueBuilder",
|
||||
name: "FeatureValueBuilder",
|
||||
path: "/crud/feature/value-builder",
|
||||
component: "/crud/feature/value-builder/index.vue"
|
||||
},
|
||||
{
|
||||
title: "列设置",
|
||||
name: "FeatureColumnsSet",
|
||||
path: "/crud/feature/columns-set",
|
||||
component: "/crud/feature/columns-set/index.vue"
|
||||
},
|
||||
{
|
||||
title: "本地化编辑",
|
||||
name: "FeatureLocal",
|
||||
path: "/crud/feature/local",
|
||||
component: "/crud/feature/local/index.vue"
|
||||
},
|
||||
{
|
||||
title: "v-model",
|
||||
name: "FeatureVModel",
|
||||
path: "/crud/feature/v-model",
|
||||
component: "/crud/feature/local-v-model/index.vue"
|
||||
},
|
||||
{
|
||||
title: "自定义删除",
|
||||
name: "FeatureRemove",
|
||||
path: "/crud/feature/remove",
|
||||
component: "/crud/feature/remove/index.vue"
|
||||
},
|
||||
{
|
||||
title: "调整列宽",
|
||||
name: "FeatureColumnResize",
|
||||
path: "/crud/feature/column-resize",
|
||||
component: "/crud/feature/column-resize/index.vue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "插槽",
|
||||
name: "Slots",
|
||||
path: "/crud/slots",
|
||||
redirect: "/crud/slots/layout",
|
||||
meta: {
|
||||
icon: "ion:extension-puzzle-outline"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "页面占位插槽",
|
||||
name: "SlotsLayout",
|
||||
path: "/crud/slots/layout",
|
||||
component: "/crud/slots/layout/index.vue"
|
||||
},
|
||||
{
|
||||
title: "表单占位插槽",
|
||||
name: "SlotsForm",
|
||||
path: "/crud/slots/form",
|
||||
component: "/crud/slots/form/index.vue"
|
||||
},
|
||||
{
|
||||
title: "查询字段插槽",
|
||||
name: "SlotsSearch",
|
||||
path: "/crud/slots/search",
|
||||
component: "/crud/slots/search/index.vue"
|
||||
},
|
||||
{
|
||||
title: "单元格插槽",
|
||||
name: "SlotsCell",
|
||||
path: "/crud/slots/cell",
|
||||
component: "/crud/slots/cell/index.vue"
|
||||
},
|
||||
{
|
||||
title: "表单字段插槽",
|
||||
name: "SlotsFormItem",
|
||||
path: "/crud/slots/form-item",
|
||||
component: "/crud/slots/form-item/index.vue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "复杂需求",
|
||||
name: "Advanced",
|
||||
path: "/crud/advanced",
|
||||
redirect: "/crud/advanced/linkage",
|
||||
meta: {
|
||||
icon: "ion:flame-outline"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "选择联动",
|
||||
name: "AdvancedLinkage",
|
||||
path: "/crud/advanced/linkage",
|
||||
component: "/crud/advanced/linkage/index.vue"
|
||||
},
|
||||
{
|
||||
title: "后台加载crud",
|
||||
name: "AdvancedFormBackend",
|
||||
path: "/crud/advanced/from-backend",
|
||||
component: "/crud/advanced/from-backend/index.vue"
|
||||
},
|
||||
{
|
||||
title: "本地分页",
|
||||
name: "AdvancedLocalPagination",
|
||||
path: "/crud/advanced/local-pagination",
|
||||
component: "/crud/advanced/local-pagination/index.vue"
|
||||
},
|
||||
{
|
||||
title: "嵌套子表格",
|
||||
name: "AdvancedNest",
|
||||
path: "/crud/advanced/nest",
|
||||
component: "/crud/advanced/nest/index.vue"
|
||||
},
|
||||
{
|
||||
title: "对话框中显示crud",
|
||||
name: "AdvancedInDialog",
|
||||
path: "/crud/advanced/in-dialog",
|
||||
component: "/crud/advanced/in-dialog/index.vue"
|
||||
},
|
||||
{
|
||||
title: "大量数据",
|
||||
name: "AdvancedBigData",
|
||||
path: "/crud/advanced/big-data",
|
||||
component: "/crud/advanced/big-data/index.vue"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,61 @@
|
||||
import LayoutPass from "/@/layout/layout-pass.vue";
|
||||
|
||||
export const sysResources = [
|
||||
{
|
||||
title: "系统管理",
|
||||
name: "sys",
|
||||
path: "/sys",
|
||||
redirect: "/sys/authority",
|
||||
component: LayoutPass,
|
||||
meta: {
|
||||
icon: "ion:settings-outline",
|
||||
permission: "sys"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "权限管理",
|
||||
name: "authority",
|
||||
path: "/sys/authority",
|
||||
redirect: "/sys/authority/permission",
|
||||
meta: {
|
||||
icon: "ion:ribbon-outline",
|
||||
//需要校验权限
|
||||
permission: "sys:auth"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "权限资源管理",
|
||||
name: "permission",
|
||||
meta: {
|
||||
icon: "ion:list-outline",
|
||||
//需要校验权限
|
||||
permission: "sys:auth:per:view"
|
||||
},
|
||||
path: "/sys/authority/permission",
|
||||
component: "/sys/authority/permission/index.vue"
|
||||
},
|
||||
{
|
||||
title: "角色管理",
|
||||
name: "role",
|
||||
meta: {
|
||||
icon: "ion:people-outline",
|
||||
permission: "sys:auth:role:view"
|
||||
},
|
||||
path: "/sys/authority/role",
|
||||
component: "/sys/authority/role/index.vue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "用户管理",
|
||||
name: "user",
|
||||
meta: {
|
||||
icon: "ion:person-outline",
|
||||
permission: "sys:auth:user:view"
|
||||
},
|
||||
path: "/sys/authority/user",
|
||||
component: "/sys/authority/user/index.vue"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
import LayoutOutside from "/src/layout/layout-outside.vue";
|
||||
import Error404 from "/src/views/framework/error/404.vue";
|
||||
const errorPage = [{ path: "/:pathMatch(.*)*", name: "not-found", component: Error404 }];
|
||||
export const outsideResource = [
|
||||
{
|
||||
title: "outside",
|
||||
name: "outside",
|
||||
path: "/outside",
|
||||
component: LayoutOutside,
|
||||
children: [
|
||||
{
|
||||
meta: {
|
||||
title: "登录"
|
||||
},
|
||||
name: "login",
|
||||
path: "/login",
|
||||
component: "/framework/login/index.vue"
|
||||
}
|
||||
]
|
||||
},
|
||||
...errorPage
|
||||
];
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
declare module "*.vue" {
|
||||
import { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createPinia } from "pinia";
|
||||
const store = createPinia();
|
||||
export default {
|
||||
install(app) {
|
||||
app.use(store);
|
||||
}
|
||||
};
|
||||
|
||||
export { store };
|
||||
@@ -0,0 +1,436 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { cloneDeep, get, uniq } from "lodash-es";
|
||||
import router from "/src/router";
|
||||
import { frameworkRoutes } from "/src/router/resolve";
|
||||
// @ts-ignore
|
||||
import { LocalStorage } from "/src/utils/util.storage";
|
||||
import { useUserStore } from "/src/store/modules/user";
|
||||
const OPENED_CACHE_KEY = "TABS_OPENED";
|
||||
|
||||
interface PageState {
|
||||
// 可以在多页 tab 模式下显示的页面
|
||||
pool: Array<any>;
|
||||
// 当前显示的多页面列表
|
||||
opened: Array<any>;
|
||||
// 已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201
|
||||
openedLoaded: boolean;
|
||||
// 当前页面
|
||||
current: "";
|
||||
// 需要缓存的页面 name
|
||||
keepAlive: Array<any>;
|
||||
inited: boolean;
|
||||
}
|
||||
// 判定是否需要缓存
|
||||
const isKeepAlive = (data) => get(data, "meta.cache", false);
|
||||
|
||||
export const usePageStore = defineStore({
|
||||
id: "app.page",
|
||||
state: (): PageState => ({
|
||||
// 可以在多页 tab 模式下显示的页面
|
||||
pool: [],
|
||||
// 当前显示的多页面列表
|
||||
opened: [
|
||||
{
|
||||
name: "index",
|
||||
fullPath: "/index",
|
||||
meta: {
|
||||
title: "首页",
|
||||
auth: false
|
||||
}
|
||||
}
|
||||
],
|
||||
// 已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201
|
||||
openedLoaded: false,
|
||||
// 当前页面
|
||||
current: "",
|
||||
// 需要缓存的页面 name
|
||||
keepAlive: [],
|
||||
inited: false
|
||||
}),
|
||||
getters: {
|
||||
getOpened() {
|
||||
// @ts-ignore
|
||||
return this.opened;
|
||||
},
|
||||
getCurrent(): string {
|
||||
return this.current;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
* @description 确认已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201
|
||||
* @param {Object} context
|
||||
*/
|
||||
async isLoaded() {
|
||||
if (this.openedLoaded) {
|
||||
return true;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const timer = setInterval(() => {
|
||||
if (this.openedLoaded) {
|
||||
resolve(clearInterval(timer));
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @class opened
|
||||
* @description 从持久化数据载入标签页列表
|
||||
* @param {Object} context
|
||||
*/
|
||||
async openedLoad() {
|
||||
// store 赋值
|
||||
const value = LocalStorage.get(this.getStorageKey());
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
// 在处理函数中进行数据优化 过滤掉现在已经失效的页签或者已经改变了信息的页签
|
||||
// 以 fullPath 字段为准
|
||||
// 如果页面过多的话可能需要优化算法
|
||||
// valid 有效列表 1, 1, 0, 1 => 有效, 有效, 失效, 有效
|
||||
const valid: Array<number> = [];
|
||||
// 处理数据
|
||||
this.opened = value
|
||||
.map((opened) => {
|
||||
// 忽略首页
|
||||
if (opened.fullPath === "/index") {
|
||||
valid.push(1);
|
||||
return opened;
|
||||
}
|
||||
// 尝试在所有的支持多标签页的页面里找到 name 匹配的页面
|
||||
const find = this.pool.find((item) => item.name === opened.name);
|
||||
// 记录有效或无效信息
|
||||
valid.push(find ? 1 : 0);
|
||||
// 返回合并后的数据 新的覆盖旧的
|
||||
// 新的数据中一般不会携带 params 和 query, 所以旧的参数会留存
|
||||
return Object.assign({}, opened, find);
|
||||
})
|
||||
.filter((opened, index) => valid[index] === 1);
|
||||
// 标记已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201
|
||||
this.openedLoaded = true;
|
||||
// 根据 opened 数据生成缓存设置
|
||||
this.keepAliveRefresh();
|
||||
},
|
||||
|
||||
getStorageKey() {
|
||||
const userStore = useUserStore();
|
||||
const userId = userStore.getUserInfo?.id ?? "anonymous";
|
||||
return OPENED_CACHE_KEY + ":" + userId;
|
||||
},
|
||||
/**
|
||||
* 将 opened 属性赋值并持久化 在这之前请先确保已经更新了 state.opened
|
||||
* @param {Object} context
|
||||
*/
|
||||
async opened2db() {
|
||||
// 设置数据
|
||||
|
||||
LocalStorage.set(this.getStorageKey(), this.opened);
|
||||
},
|
||||
/**
|
||||
* @class opened
|
||||
* @description 更新页面列表上的某一项
|
||||
* @param {Object} context
|
||||
* @param {Object} payload { index, params, query, fullPath } 路由信息
|
||||
*/
|
||||
async openedUpdate({ index, params, query, fullPath }) {
|
||||
// 更新页面列表某一项
|
||||
const page = this.opened[index];
|
||||
page.params = params || page.params;
|
||||
page.query = query || page.query;
|
||||
page.fullPath = fullPath || page.fullPath;
|
||||
this.opened.splice(index, 1, page);
|
||||
// 持久化
|
||||
await this.opened2db();
|
||||
},
|
||||
/**
|
||||
* @class opened
|
||||
* @description 重排页面列表上的某一项
|
||||
* @param {Object} context
|
||||
* @param {Object} payload { oldIndex, newIndex } 位置信息
|
||||
*/
|
||||
async openedSort({ oldIndex, newIndex }) {
|
||||
// 重排页面列表某一项
|
||||
const page = this.opened[oldIndex];
|
||||
this.opened.splice(oldIndex, 1);
|
||||
this.opened.splice(newIndex, 0, page);
|
||||
// 持久化
|
||||
await this.opened2db();
|
||||
},
|
||||
/**
|
||||
* @class opened
|
||||
* @description 新增一个 tag (打开一个页面)
|
||||
* @param {Object} context
|
||||
* @param {Object} payload new tag info
|
||||
*/
|
||||
async add({ tag, params, query, fullPath }) {
|
||||
// 设置新的 tag 在新打开一个以前没打开过的页面时使用
|
||||
const newTag = tag;
|
||||
newTag.params = params || newTag.params;
|
||||
newTag.query = query || newTag.query;
|
||||
newTag.fullPath = fullPath || newTag.fullPath;
|
||||
// 添加进当前显示的页面数组
|
||||
this.opened.push(newTag);
|
||||
// 如果这个页面需要缓存 将其添加到缓存设置
|
||||
if (isKeepAlive(newTag)) {
|
||||
this.keepAlivePush(tag.name);
|
||||
}
|
||||
// 持久化
|
||||
await this.opened2db();
|
||||
},
|
||||
/**
|
||||
* @class current
|
||||
* @description 打开一个新的页面
|
||||
* @param {Object} context
|
||||
* @param {Object} payload 从路由钩子的 to 对象上获取 { name, params, query, fullPath, meta } 路由信息
|
||||
*/
|
||||
async open({ name, params, query, fullPath, meta }) {
|
||||
// 已经打开的页面
|
||||
const opened = this.opened;
|
||||
// 判断此页面是否已经打开 并且记录位置
|
||||
let pageOpendIndex = 0;
|
||||
const pageOpend = opened.find((page, index) => {
|
||||
const same = page.fullPath === fullPath;
|
||||
pageOpendIndex = same ? index : pageOpendIndex;
|
||||
return same;
|
||||
});
|
||||
if (pageOpend) {
|
||||
// 页面以前打开过
|
||||
await this.openedUpdate({
|
||||
index: pageOpendIndex,
|
||||
params,
|
||||
query,
|
||||
fullPath
|
||||
});
|
||||
} else {
|
||||
// 页面以前没有打开过
|
||||
const page = this.pool.find((t) => t.name === name);
|
||||
// 如果这里没有找到 page 代表这个路由虽然在框架内 但是不参与标签页显示
|
||||
if (page) {
|
||||
this.add({
|
||||
tag: Object.assign({}, page),
|
||||
params,
|
||||
query,
|
||||
fullPath
|
||||
});
|
||||
}
|
||||
}
|
||||
// 如果这个页面需要缓存 将其添加到缓存设置
|
||||
if (isKeepAlive({ meta })) {
|
||||
this.keepAlivePush(name);
|
||||
}
|
||||
// 设置当前的页面
|
||||
this.currentSet(fullPath);
|
||||
},
|
||||
/**
|
||||
* @class opened
|
||||
* @description 关闭一个 tag (关闭一个页面)
|
||||
* @param {Object} context
|
||||
* @param {Object} payload { tagName: 要关闭的标签名字 }
|
||||
*/
|
||||
async close({ tagName }) {
|
||||
// 预定下个新页面
|
||||
let newPage = {};
|
||||
const isCurrent = this.current === tagName;
|
||||
// 如果关闭的页面就是当前显示的页面
|
||||
if (isCurrent) {
|
||||
// 去找一个新的页面
|
||||
const len = this.opened.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (this.opened[i].fullPath === tagName) {
|
||||
newPage = i < len - 1 ? this.opened[i + 1] : this.opened[i - 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 找到这个页面在已经打开的数据里是第几个
|
||||
const index = this.opened.findIndex((page) => page.fullPath === tagName);
|
||||
if (index >= 0) {
|
||||
// 如果这个页面是缓存的页面 将其在缓存设置中删除
|
||||
this.keepAliveRemove(this.opened[index].name);
|
||||
// 更新数据 删除关闭的页面
|
||||
this.opened.splice(index, 1);
|
||||
}
|
||||
// 持久化
|
||||
await this.opened2db();
|
||||
// 决定最后停留的页面
|
||||
if (isCurrent) {
|
||||
// @ts-ignore
|
||||
const { name = "index", params = {}, query = {} } = newPage;
|
||||
const routerObj = { name, params, query };
|
||||
await router.push(routerObj);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @class opened
|
||||
* @description 关闭当前标签左边的标签
|
||||
* @param opts
|
||||
*/
|
||||
async closeLeft(opts = {}) {
|
||||
await this.closeByCondition({
|
||||
condition({ i, currentIndex }) {
|
||||
return i >= currentIndex;
|
||||
},
|
||||
...opts
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @class opened
|
||||
* @description 关闭当前标签右边的标签
|
||||
* @param opts
|
||||
*/
|
||||
async closeRight(opts = {}) {
|
||||
await this.closeByCondition({
|
||||
condition({ i, currentIndex }) {
|
||||
return currentIndex >= i;
|
||||
},
|
||||
...opts
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @class opened
|
||||
* @description 关闭当前标签右边的标签
|
||||
* @param opts
|
||||
*/
|
||||
async closeByCondition(opts = {}) {
|
||||
// @ts-ignore
|
||||
const { pageSelect, condition } = opts;
|
||||
const pageAim = pageSelect || this.current;
|
||||
let currentIndex = 0;
|
||||
this.opened.forEach((page, index) => {
|
||||
if (page.fullPath === pageAim) currentIndex = index;
|
||||
});
|
||||
// 删除打开的页面 并在缓存设置中删除
|
||||
for (let i = this.opened.length - 1; i >= 0; i--) {
|
||||
if (this.opened[i].name === "index" || condition({ i, currentIndex })) {
|
||||
continue;
|
||||
}
|
||||
this.keepAliveRemove(this.opened[i].name);
|
||||
this.opened.splice(i, 1);
|
||||
}
|
||||
// 持久化
|
||||
await this.opened2db();
|
||||
// 设置当前的页面
|
||||
this.current = pageAim;
|
||||
// @ts-ignore
|
||||
if (router.currentRoute.fullPath !== pageAim) await router.push(pageAim);
|
||||
},
|
||||
/**
|
||||
* @class opened
|
||||
* @description 关闭当前激活之外的 tag
|
||||
* @param opts
|
||||
*/
|
||||
async closeOther(opts = {}) {
|
||||
await this.closeByCondition({
|
||||
condition({ i, currentIndex }) {
|
||||
return currentIndex === i;
|
||||
},
|
||||
...opts
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @class opened
|
||||
* @description 关闭所有 tag
|
||||
* @param {Object} context
|
||||
*/
|
||||
async closeAll() {
|
||||
// 删除打开的页面 并在缓存设置中删除
|
||||
for (let i = this.opened.length - 1; i >= 0; i--) {
|
||||
if (this.opened[i].name === "index") {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.keepAliveRemove(this.opened[i].name);
|
||||
this.opened.splice(i, 1);
|
||||
}
|
||||
// 持久化
|
||||
await this.opened2db();
|
||||
// 关闭所有的标签页后需要判断一次现在是不是在首页
|
||||
// @ts-ignore
|
||||
if (router.currentRoute.name !== "index") {
|
||||
await router.push({ name: "index" });
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @class keepAlive
|
||||
* @description 从已经打开的页面记录中更新需要缓存的页面记录
|
||||
* @param {Object} state state
|
||||
*/
|
||||
keepAliveRefresh() {
|
||||
this.keepAlive = this.opened.filter((item) => isKeepAlive(item)).map((e) => e.name);
|
||||
console.log("keep alive:", this.keepAlive);
|
||||
},
|
||||
/**
|
||||
* @description 删除一个页面的缓存设置
|
||||
* @param {Object} state state
|
||||
* @param {String} name name
|
||||
*/
|
||||
keepAliveRemove(name) {
|
||||
const list = cloneDeep(this.keepAlive);
|
||||
const index = list.findIndex((item) => item === name);
|
||||
if (index !== -1) {
|
||||
list.splice(index, 1);
|
||||
this.keepAlive = list;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description 增加一个页面的缓存设置
|
||||
* @param {Object} state state
|
||||
* @param {String} name name
|
||||
*/
|
||||
keepAlivePush(name) {
|
||||
const keep = cloneDeep(this.keepAlive);
|
||||
keep.push(name);
|
||||
this.keepAlive = uniq(keep);
|
||||
},
|
||||
/**
|
||||
* @description 清空页面缓存设置
|
||||
* @param {Object} state state
|
||||
*/
|
||||
keepAliveClean() {
|
||||
this.keepAlive = [];
|
||||
},
|
||||
/**
|
||||
* @class current
|
||||
* @description 设置当前激活的页面 fullPath
|
||||
* @param {Object} state state
|
||||
* @param {String} fullPath new fullPath
|
||||
*/
|
||||
currentSet(fullPath) {
|
||||
this.current = fullPath;
|
||||
},
|
||||
/**
|
||||
* @class pool
|
||||
* @description 保存 pool (候选池)
|
||||
* @param {Object} state state
|
||||
* @param {Array} routes routes
|
||||
*/
|
||||
async init(routes) {
|
||||
if (this.inited) {
|
||||
return;
|
||||
}
|
||||
this.inited = true;
|
||||
if (routes == null) {
|
||||
//不能用全部的routes,只能是framework内的
|
||||
routes = frameworkRoutes;
|
||||
}
|
||||
|
||||
const pool = [];
|
||||
const push = function (routes) {
|
||||
routes.forEach((route) => {
|
||||
if (route.children && route.children.length > 0) {
|
||||
push(route.children);
|
||||
} else {
|
||||
if (!route.hidden) {
|
||||
const { meta, name, path } = route;
|
||||
// @ts-ignore
|
||||
pool.push({ meta, name, path });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
push(routes);
|
||||
this.pool = pool;
|
||||
await this.openedLoad();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
import { defineStore } from "pinia";
|
||||
// @ts-ignore
|
||||
import { frameworkMenus, headerMenus, filterMenus, findMenus } from "/src/router/resolve";
|
||||
import _ from "lodash-es";
|
||||
import { mitter } from "/src/utils/util.mitt";
|
||||
//监听注销事件
|
||||
mitter.on("app.logout", () => {
|
||||
const resourceStore = useResourceStore();
|
||||
resourceStore.clear();
|
||||
});
|
||||
|
||||
interface ResourceState {
|
||||
frameworkMenus: Array<any>;
|
||||
headerMenus: Array<any>;
|
||||
asideMenus: Array<any>;
|
||||
fixedAsideMenus: Array<any>;
|
||||
inited: boolean;
|
||||
currentAsidePath: string;
|
||||
}
|
||||
|
||||
export const useResourceStore = defineStore({
|
||||
id: "app.resource",
|
||||
state: (): ResourceState => ({
|
||||
// user info
|
||||
frameworkMenus: [],
|
||||
headerMenus: [],
|
||||
asideMenus: [],
|
||||
fixedAsideMenus: [],
|
||||
inited: false,
|
||||
currentAsidePath: ""
|
||||
}),
|
||||
getters: {
|
||||
getAsideMenus() {
|
||||
return this.asideMenus;
|
||||
},
|
||||
getHeaderMenus() {
|
||||
return this.headerMenus;
|
||||
},
|
||||
getFrameworkMenus() {
|
||||
return this.frameworkMenus;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
clear() {
|
||||
this.inited = false;
|
||||
},
|
||||
/**
|
||||
* 初始化资源
|
||||
*/
|
||||
init() {
|
||||
if (this.inited) {
|
||||
return;
|
||||
}
|
||||
this.inited = true;
|
||||
|
||||
const showMenus = _.cloneDeep(frameworkMenus[0].children);
|
||||
this.frameworkMenus = filterMenus(showMenus, (item) => {
|
||||
return item?.meta?.showOnHeader !== false;
|
||||
});
|
||||
|
||||
this.fixedAsideMenus = findMenus(showMenus, (item) => {
|
||||
return item?.meta?.fixedAside === true;
|
||||
});
|
||||
this.headerMenus = headerMenus;
|
||||
this.setAsideMenu();
|
||||
},
|
||||
setAsideMenu(topMenu?) {
|
||||
if (this.frameworkMenus.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (topMenu == null) {
|
||||
topMenu = this.frameworkMenus[0];
|
||||
}
|
||||
const asideMenus = topMenu?.children || [];
|
||||
this.asideMenus = [...this.fixedAsideMenus, ...asideMenus];
|
||||
},
|
||||
setAsideMenuByCurrentRoute(matched) {
|
||||
const menuHeader = this.frameworkMenus;
|
||||
if (matched?.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
function findFromTree(tree, find) {
|
||||
const results: Array<any> = [];
|
||||
for (const item of tree) {
|
||||
if (find(item)) {
|
||||
results.push(item);
|
||||
return results;
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
const found = findFromTree(item.children, find);
|
||||
if (found) {
|
||||
results.push(item);
|
||||
return results.concat(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const matchedPath = matched[1].path;
|
||||
const _side = findFromTree(menuHeader, (menu) => menu.path === matchedPath);
|
||||
if (_side?.length > 0) {
|
||||
if (this.currentAsidePath === _side[0]) {
|
||||
return;
|
||||
}
|
||||
this.currentAsidePath = _side[0];
|
||||
this.setAsideMenu(_side[0]);
|
||||
}
|
||||
},
|
||||
filterByPermission(permissions) {
|
||||
this.frameworkMenus = this.filterChildrenByPermission(this.frameworkMenus, permissions);
|
||||
},
|
||||
filterChildrenByPermission(list, permissions) {
|
||||
const menus = list.filter((item) => {
|
||||
if (item?.meta?.permission) {
|
||||
return permissions.includes(item.meta.permission);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
for (const menu of menus) {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menu.children = this.filterChildrenByPermission(menu.children, permissions);
|
||||
}
|
||||
}
|
||||
return menus;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { defineStore } from "pinia";
|
||||
// @ts-ignore
|
||||
import { LocalStorage } from "/src/utils/util.storage";
|
||||
// import { replaceStyleVariables } from "vite-plugin-theme/es/client";
|
||||
|
||||
// import { getThemeColors, generateColors } from "/src/../build/theme-colors";
|
||||
//
|
||||
// import { mixLighten, mixDarken, tinycolor } from "vite-plugin-theme/es/colorUtils";
|
||||
|
||||
// export async function changeTheme(color?: string) {
|
||||
// if (color == null) {
|
||||
// return;
|
||||
// }
|
||||
// const colors = generateColors({
|
||||
// mixDarken,
|
||||
// mixLighten,
|
||||
// tinycolor,
|
||||
// color
|
||||
// });
|
||||
//
|
||||
// return await replaceStyleVariables({
|
||||
// colorVariables: [...getThemeColors(color), ...colors]
|
||||
// });
|
||||
// }
|
||||
|
||||
interface SettingState {
|
||||
theme: any;
|
||||
}
|
||||
|
||||
const SETTING_THEME_KEY = "SETTING_THEME";
|
||||
export const useSettingStore = defineStore({
|
||||
id: "app.setting",
|
||||
state: (): SettingState => ({
|
||||
// user info
|
||||
theme: null
|
||||
}),
|
||||
getters: {
|
||||
getTheme(): any {
|
||||
return this.theme || LocalStorage.get(SETTING_THEME_KEY) || {};
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
persistTheme() {
|
||||
LocalStorage.set(SETTING_THEME_KEY, this.getTheme);
|
||||
},
|
||||
async setTheme(theme?: Object) {
|
||||
if (theme == null) {
|
||||
theme = this.getTheme;
|
||||
}
|
||||
this.theme = theme;
|
||||
this.persistTheme();
|
||||
// await changeTheme(this.theme.primaryColor);
|
||||
},
|
||||
async setPrimaryColor(color) {
|
||||
const theme = this.theme;
|
||||
theme.primaryColor = color;
|
||||
await this.setTheme();
|
||||
},
|
||||
async init() {
|
||||
await this.setTheme(this.getTheme);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { store } from "../index";
|
||||
import router from "../../router";
|
||||
// @ts-ignore
|
||||
import { LocalStorage } from "/src/utils/util.storage";
|
||||
// @ts-ignore
|
||||
import * as UserApi from "/src/api/modules/api.user";
|
||||
// @ts-ignore
|
||||
import { LoginReq, UserInfoRes } from "/@/api/modules/api.user";
|
||||
import { Modal } from "ant-design-vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
import { mitter } from "/src/utils/util.mitt";
|
||||
|
||||
interface UserState {
|
||||
userInfo: Nullable<UserInfoRes>;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
const USER_INFO_KEY = "USER_INFO";
|
||||
const TOKEN_KEY = "TOKEN";
|
||||
export const useUserStore = defineStore({
|
||||
id: "app.user",
|
||||
state: (): UserState => ({
|
||||
// user info
|
||||
userInfo: null,
|
||||
// token
|
||||
token: undefined
|
||||
}),
|
||||
getters: {
|
||||
getUserInfo(): UserInfoRes {
|
||||
return this.userInfo || LocalStorage.get(USER_INFO_KEY) || {};
|
||||
},
|
||||
getToken(): string {
|
||||
return this.token || LocalStorage.get(TOKEN_KEY);
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setToken(info: string, expire: number) {
|
||||
this.token = info;
|
||||
LocalStorage.set(TOKEN_KEY, this.token, expire);
|
||||
},
|
||||
setUserInfo(info: UserInfoRes) {
|
||||
this.userInfo = info;
|
||||
LocalStorage.set(USER_INFO_KEY, info);
|
||||
},
|
||||
resetState() {
|
||||
this.userInfo = null;
|
||||
this.token = "";
|
||||
LocalStorage.remove(TOKEN_KEY);
|
||||
LocalStorage.remove(USER_INFO_KEY);
|
||||
},
|
||||
/**
|
||||
* @description: login
|
||||
*/
|
||||
async login(params: LoginReq): Promise<any> {
|
||||
try {
|
||||
const data = await UserApi.login(params);
|
||||
const { token, expire } = data;
|
||||
|
||||
// save token
|
||||
this.setToken(token, expire);
|
||||
// get user info
|
||||
const userInfo = await this.getUserInfoAction();
|
||||
await router.replace("/");
|
||||
mitter.emit("app.login", { userInfo, token: data });
|
||||
return userInfo;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async getUserInfoAction(): Promise<UserInfoRes> {
|
||||
const userInfo = await UserApi.mine();
|
||||
this.setUserInfo(userInfo);
|
||||
return userInfo;
|
||||
},
|
||||
/**
|
||||
* @description: logout
|
||||
*/
|
||||
logout(goLogin = true) {
|
||||
this.resetState();
|
||||
goLogin && router.push("/login");
|
||||
mitter.emit("app.logout");
|
||||
},
|
||||
|
||||
/**
|
||||
* @description: Confirm before logging out
|
||||
*/
|
||||
confirmLoginOut() {
|
||||
const { t } = useI18n();
|
||||
Modal.config({
|
||||
iconType: "warning",
|
||||
title: t("app.login.logoutTip"),
|
||||
content: t("app.login.logoutMessage"),
|
||||
onOk: async () => {
|
||||
await this.logout(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Need to be used outside the setup
|
||||
export function useUserStoreWidthOut() {
|
||||
return useUserStore(store);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/*占位,勿删*/
|
||||
@@ -0,0 +1,124 @@
|
||||
@import './theme/index.less';
|
||||
@import './theme/default.less';
|
||||
@import './scroll.less';
|
||||
@import './transition.less';
|
||||
@import './fix-windicss.less';
|
||||
svg { vertical-align: baseline; }
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
div#app {
|
||||
height: 100%
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-btn-link {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
|
||||
.ant-input-affix-wrapper {
|
||||
padding: 4px 11px;
|
||||
}
|
||||
|
||||
|
||||
.anticon {
|
||||
vertical-align: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
.text-center{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.red{
|
||||
color:red
|
||||
}
|
||||
|
||||
.font12{
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bg-gray{
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.bg-white{
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.fs-page-header{
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
|
||||
.ant-btn{
|
||||
.fs-iconify{
|
||||
font-size:16px;
|
||||
margin-right:3px
|
||||
}
|
||||
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.ant-timeline{
|
||||
.fs-iconify{
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.mt-10{
|
||||
margin-top:10px;
|
||||
}
|
||||
.ml-5{
|
||||
margin-left:5px;
|
||||
}
|
||||
.ml-10{
|
||||
margin-left: 10px;
|
||||
}
|
||||
.mtb-5{
|
||||
margin: 5px 0 5px 0;
|
||||
}
|
||||
|
||||
.mb-10{
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mlr-5{
|
||||
margin: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
.gray{
|
||||
color:gray;
|
||||
}
|
||||
.green{
|
||||
color:green;
|
||||
}
|
||||
.blue{
|
||||
color:blue;
|
||||
}
|
||||
.red{
|
||||
color:red
|
||||
}
|
||||
.yellow{
|
||||
color:yellow;
|
||||
}
|
||||
|
||||
.ml-2{
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.font-20{
|
||||
font-size:20px
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
img.ant-image-preview-img{
|
||||
display: initial;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
width: 8px;
|
||||
background: rgba(#101F1C, 0.1);
|
||||
-webkit-border-radius: 2em;
|
||||
-moz-border-radius: 2em;
|
||||
border-radius: 2em;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
// background-color: rgba(#101F1C, 0.5);
|
||||
background-clip: padding-box;
|
||||
min-height: 28px;
|
||||
-webkit-border-radius: 2em;
|
||||
-moz-border-radius: 2em;
|
||||
border-radius: 2em;
|
||||
background-color: #b3b3b3;
|
||||
box-shadow: 0px 1px 1px #eee inset;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
|
||||
//background-color: rgba(#101F1C, 1);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
.ant-layout{
|
||||
background-color: @bg-color;
|
||||
}
|
||||
.ant-layout-header {
|
||||
background-color: @bg-color
|
||||
}
|
||||
.ant-layout-sider {
|
||||
background-color:@bg-color
|
||||
}
|
||||
.ant-menu{
|
||||
background-color: @bg-color;
|
||||
&.ant-menu-submenu-popup{
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.aside-menu{
|
||||
.ant-menu-submenu > .ant-menu{
|
||||
background-color:@bg-color
|
||||
}
|
||||
|
||||
.ant-menu-item-active{
|
||||
background-color: @bg-menu-item-color;
|
||||
}
|
||||
.ant-menu-item-selected{
|
||||
background-color: @bg-menu-item-color !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@primary-color: #1890ff;
|
||||
// theme
|
||||
@bg-color: #ebf1f6;
|
||||
@bg-menu-item-color:hsla(0,0%,100%,.5);
|
||||
@@ -0,0 +1,36 @@
|
||||
//.v-enter-from,
|
||||
//.v-leave-to {
|
||||
// opacity: 0;
|
||||
//}
|
||||
//
|
||||
//.v-leave-from,
|
||||
//.v-enter-to {
|
||||
// opacity: 1;
|
||||
//}
|
||||
// 过渡动画 横向渐变
|
||||
.fade-transverse-leave-active,
|
||||
.fade-transverse-enter-active {
|
||||
transition: all .5s;
|
||||
}
|
||||
.fade-transverse-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
.fade-transverse-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
// 过渡动画 缩放渐变
|
||||
.fade-scale-leave-active,
|
||||
.fade-scale-enter-active {
|
||||
transition: all .3s;
|
||||
}
|
||||
.fade-scale-enter {
|
||||
opacity: 0;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
.fade-scale-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import type {
|
||||
ComponentRenderProxy,
|
||||
VNode,
|
||||
ComponentPublicInstance,
|
||||
FunctionalComponent,
|
||||
PropType as VuePropType,
|
||||
} from 'vue';
|
||||
|
||||
declare global {
|
||||
const __APP_INFO__: {
|
||||
pkg: {
|
||||
name: string;
|
||||
version: string;
|
||||
dependencies: Recordable<string>;
|
||||
devDependencies: Recordable<string>;
|
||||
};
|
||||
lastBuildTime: string;
|
||||
};
|
||||
declare interface Window {
|
||||
// Global vue app instance
|
||||
__APP__: App<Element>;
|
||||
}
|
||||
|
||||
// vue
|
||||
declare type PropType<T> = VuePropType<T>;
|
||||
|
||||
export type Writable<T> = {
|
||||
-readonly [P in keyof T]: T[P];
|
||||
};
|
||||
|
||||
declare type Nullable<T> = T | null;
|
||||
declare type NonNullable<T> = T extends null | undefined ? never : T;
|
||||
declare type Recordable<T = any> = Record<string, T>;
|
||||
declare type ReadonlyRecordable<T = any> = {
|
||||
readonly [key: string]: T;
|
||||
};
|
||||
declare type Indexable<T = any> = {
|
||||
[key: string]: T;
|
||||
};
|
||||
declare type DeepPartial<T> = {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
};
|
||||
declare type TimeoutHandle = ReturnType<typeof setTimeout>;
|
||||
declare type IntervalHandle = ReturnType<typeof setInterval>;
|
||||
|
||||
declare interface ChangeEvent extends Event {
|
||||
target: HTMLInputElement;
|
||||
}
|
||||
|
||||
declare interface WheelEvent {
|
||||
path?: EventTarget[];
|
||||
}
|
||||
interface ImportMetaEnv extends ViteEnv {
|
||||
__: unknown;
|
||||
}
|
||||
|
||||
declare interface ViteEnv {
|
||||
VITE_PORT: number;
|
||||
VITE_USE_MOCK: boolean;
|
||||
VITE_USE_PWA: boolean;
|
||||
VITE_PUBLIC_PATH: string;
|
||||
VITE_PROXY: [string, string][];
|
||||
VITE_GLOB_APP_TITLE: string;
|
||||
VITE_GLOB_APP_SHORT_NAME: string;
|
||||
VITE_USE_CDN: boolean;
|
||||
VITE_DROP_CONSOLE: boolean;
|
||||
VITE_BUILD_COMPRESS: 'gzip' | 'brotli' | 'none';
|
||||
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean;
|
||||
VITE_LEGACY: boolean;
|
||||
VITE_USE_IMAGEMIN: boolean;
|
||||
VITE_GENERATE_UI: string;
|
||||
}
|
||||
|
||||
declare function parseInt(s: string | number, radix?: number): number;
|
||||
|
||||
declare function parseFloat(string: string | number): number;
|
||||
|
||||
namespace JSX {
|
||||
// tslint:disable no-empty-interface
|
||||
type Element = VNode;
|
||||
// tslint:disable no-empty-interface
|
||||
type ElementClass = ComponentRenderProxy;
|
||||
interface ElementAttributesProperty {
|
||||
$props: any;
|
||||
}
|
||||
interface IntrinsicElements {
|
||||
[elem: string]: any;
|
||||
}
|
||||
interface IntrinsicAttributes {
|
||||
[elem: string]: any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vue' {
|
||||
export type JSXComponent<Props = any> =
|
||||
| { new (): ComponentPublicInstance<Props> }
|
||||
| FunctionalComponent<Props>;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as envs from "./util.env";
|
||||
import * as sites from "./util.site";
|
||||
import * as storages from "./util.storage";
|
||||
import * as commons from "./util.common";
|
||||
import * as mitt from "./util.mitt";
|
||||
export const util = {
|
||||
...envs,
|
||||
...sites,
|
||||
...storages,
|
||||
...commons,
|
||||
...mitt
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import _ from "lodash-es";
|
||||
export default {
|
||||
arrayToMap(array) {
|
||||
if (!array) {
|
||||
return {};
|
||||
}
|
||||
if (!_.isArray(array)) {
|
||||
return array;
|
||||
}
|
||||
const map = {};
|
||||
for (const item of array) {
|
||||
if (item.key) {
|
||||
map[item.key] = item;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
},
|
||||
mapToArray(map) {
|
||||
if (!map) {
|
||||
return [];
|
||||
}
|
||||
if (_.isArray(map)) {
|
||||
return map;
|
||||
}
|
||||
const array: any = [];
|
||||
for (const key in map) {
|
||||
const item = map[key];
|
||||
item.key = key;
|
||||
array.push(item);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import _ from "lodash-es";
|
||||
export function getEnvValue(key) {
|
||||
// @ts-ignore
|
||||
return import.meta.env["VITE_APP_" + key];
|
||||
}
|
||||
|
||||
export class EnvConfig {
|
||||
API;
|
||||
MODE;
|
||||
STORAGE;
|
||||
TITLE;
|
||||
PM_ENABLED;
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// @ts-ignore
|
||||
_.forEach(import.meta.env, (value, key) => {
|
||||
if (key.startsWith("VITE_APP")) {
|
||||
key = key.replace("VITE_APP_", "");
|
||||
this[key] = value;
|
||||
}
|
||||
});
|
||||
// @ts-ignore
|
||||
this.MODE = import.meta.env.MODE;
|
||||
}
|
||||
|
||||
get(key, defaultValue) {
|
||||
return this[key] ?? defaultValue;
|
||||
}
|
||||
isDev() {
|
||||
return this.MODE === "development" || this.MODE === "debug";
|
||||
}
|
||||
isProd() {
|
||||
return this.MODE === "production";
|
||||
}
|
||||
}
|
||||
|
||||
export const env = new EnvConfig();
|
||||
@@ -0,0 +1,2 @@
|
||||
import mitt from "mitt";
|
||||
export const mitter = mitt();
|
||||
@@ -0,0 +1,11 @@
|
||||
import { env } from "./util.env";
|
||||
export const site = {
|
||||
/**
|
||||
* @description 更新标题
|
||||
* @param {String} title 标题
|
||||
*/
|
||||
title: function (titleText) {
|
||||
const processTitle = env.TITLE || "FsAdmin";
|
||||
window.document.title = `${processTitle}${titleText ? ` | ${titleText}` : ""}`;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
import { env } from "./util.env";
|
||||
function isNullOrUnDef(value) {
|
||||
return value == null;
|
||||
}
|
||||
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7;
|
||||
export interface CreateStorageParams {
|
||||
prefixKey: string;
|
||||
storage: Storage;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
*Cache class
|
||||
*Construction parameters can be passed into sessionStorage, localStorage,
|
||||
* @class Cache
|
||||
* @example
|
||||
*/
|
||||
export class WebStorage {
|
||||
private storage: Storage;
|
||||
private prefixKey?: string;
|
||||
private timeout?: number;
|
||||
/**
|
||||
*
|
||||
* @param option
|
||||
*/
|
||||
constructor(option: Partial<CreateStorageParams>) {
|
||||
this.storage = option.storage ?? localStorage;
|
||||
this.prefixKey = option.prefixKey ?? getStorageShortName();
|
||||
this.timeout = option.timeout ?? DEFAULT_CACHE_TIME;
|
||||
}
|
||||
|
||||
private getKey(key: string) {
|
||||
return `${this.prefixKey}${key}`.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Set cache
|
||||
* @param {string} key
|
||||
* @param {*} value
|
||||
* @param expire
|
||||
* @expire Expiration time in seconds
|
||||
* @memberof Cache
|
||||
*/
|
||||
set(key: string, value: any, expire: number | undefined = this.timeout) {
|
||||
const stringData = JSON.stringify({
|
||||
value,
|
||||
time: Date.now(),
|
||||
expire: expire != null ? new Date().getTime() + expire * 1000 : null
|
||||
});
|
||||
this.storage.setItem(this.getKey(key), stringData);
|
||||
}
|
||||
|
||||
/**
|
||||
*Read cache
|
||||
* @param {string} key
|
||||
* @param def
|
||||
* @memberof Cache
|
||||
*/
|
||||
get(key: string, def: any = null): any {
|
||||
const val = this.storage.getItem(this.getKey(key));
|
||||
if (!val) return def;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(val);
|
||||
const { value, expire } = data;
|
||||
if (isNullOrUnDef(expire) || expire >= new Date().getTime()) {
|
||||
return value;
|
||||
}
|
||||
this.remove(key);
|
||||
} catch (e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cache based on key
|
||||
* @param {string} key
|
||||
* @memberof Cache
|
||||
*/
|
||||
remove(key: string) {
|
||||
this.storage.removeItem(this.getKey(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all caches of this instance
|
||||
*/
|
||||
clear(): void {
|
||||
this.storage.clear();
|
||||
}
|
||||
}
|
||||
export const createStorage = (option: Partial<CreateStorageParams> = {}): WebStorage => {
|
||||
return new WebStorage(option);
|
||||
};
|
||||
|
||||
export type Options = Partial<CreateStorageParams>;
|
||||
|
||||
function getStorageShortName() {
|
||||
return env.MODE + "_" + env.get("STORAGE", "certd") + "_";
|
||||
}
|
||||
|
||||
export const createSessionStorage = (options: Options = {}): WebStorage => {
|
||||
return createStorage({ storage: sessionStorage, ...options });
|
||||
};
|
||||
|
||||
export const createLocalStorage = (options: Options = {}): WebStorage => {
|
||||
return createStorage({ storage: localStorage, timeout: DEFAULT_CACHE_TIME, ...options });
|
||||
};
|
||||
|
||||
export const SessionStorage = createSessionStorage();
|
||||
export const LocalStorage = createLocalStorage();
|
||||
|
||||
export default WebStorage;
|
||||
@@ -0,0 +1,112 @@
|
||||
import * as api from "/@/views/certd/access/api";
|
||||
import { ref } from "vue";
|
||||
import { getCommonColumnDefine } from "/@/views/certd/access/common";
|
||||
|
||||
export default function ({ expose, props, ctx }) {
|
||||
const { crudBinding } = expose;
|
||||
const lastResRef = ref();
|
||||
const pageRequest = async (query) => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const editRequest = async ({ form, row }) => {
|
||||
form.id = row.id;
|
||||
form.type = props.type;
|
||||
const res = await api.UpdateObj(form);
|
||||
lastResRef.value = res;
|
||||
return res;
|
||||
};
|
||||
const delRequest = async ({ row }) => {
|
||||
return await api.DelObj(row.id);
|
||||
};
|
||||
|
||||
const addRequest = async ({ form }) => {
|
||||
form.type = props.type;
|
||||
const res = await api.AddObj(form);
|
||||
lastResRef.value = res;
|
||||
return res;
|
||||
};
|
||||
|
||||
const selectedRowKey = ref([props.modelValue]);
|
||||
// watch(
|
||||
// () => {
|
||||
// return props.modelValue;
|
||||
// },
|
||||
// (value) => {
|
||||
// selectedRowKey.value = [value];
|
||||
// },
|
||||
// {
|
||||
// immediate: true
|
||||
// }
|
||||
// );
|
||||
const onSelectChange = (changed) => {
|
||||
selectedRowKey.value = changed;
|
||||
ctx.emit("update:modelValue", changed[0]);
|
||||
};
|
||||
|
||||
const typeRef = ref("aliyun");
|
||||
const commonColumnsDefine = getCommonColumnDefine(crudBinding, typeRef);
|
||||
commonColumnsDefine.type.form.component.disabled = true;
|
||||
return {
|
||||
typeRef,
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest
|
||||
},
|
||||
toolbar: {
|
||||
show: false
|
||||
},
|
||||
search: {
|
||||
show: false
|
||||
},
|
||||
form: {
|
||||
wrapper: {
|
||||
width: "1050px"
|
||||
}
|
||||
},
|
||||
rowHandle: {
|
||||
width: "150px"
|
||||
},
|
||||
table: {
|
||||
rowSelection: {
|
||||
type: "radio",
|
||||
selectedRowKeys: selectedRowKey,
|
||||
onChange: onSelectChange
|
||||
},
|
||||
customRow: (record) => {
|
||||
return {
|
||||
onClick: () => {
|
||||
onSelectChange([record.id]);
|
||||
} // 点击行
|
||||
};
|
||||
}
|
||||
},
|
||||
columns: {
|
||||
id: {
|
||||
title: "ID",
|
||||
key: "id",
|
||||
type: "number",
|
||||
column: {
|
||||
width: 50
|
||||
},
|
||||
form: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
name: {
|
||||
title: "名称",
|
||||
search: {
|
||||
show: true
|
||||
},
|
||||
type: ["text"],
|
||||
form: {
|
||||
rules: [{ required: true, message: "请填写名称" }]
|
||||
}
|
||||
},
|
||||
...commonColumnsDefine
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<fs-page class="page-cert-access-modal">
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, onMounted, ref, watch } from "vue";
|
||||
import { useCrud, useExpose } from "@fast-crud/fast-crud";
|
||||
import createCrudOptions from "./crud";
|
||||
|
||||
export default defineComponent({
|
||||
name: "CertAccessModal",
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: "aliyun"
|
||||
},
|
||||
modelValue: {}
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, ctx) {
|
||||
// crud组件的ref
|
||||
const crudRef = ref();
|
||||
// crud 配置的ref
|
||||
const crudBinding = ref();
|
||||
// 暴露的方法
|
||||
const { expose } = useExpose({ crudRef, crudBinding });
|
||||
// 你的crud配置
|
||||
const { crudOptions, typeRef } = createCrudOptions({ expose, props, ctx });
|
||||
// 初始化crud配置
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
const { resetCrudOptions } = useCrud({ expose, crudOptions });
|
||||
// 你可以调用此方法,重新初始化crud配置
|
||||
function onTypeChanged(value) {
|
||||
typeRef.value = value;
|
||||
expose.setSearchFormData({ form: { type: value }, mergeForm: true });
|
||||
expose.doRefresh();
|
||||
}
|
||||
watch(
|
||||
() => {
|
||||
return props.type;
|
||||
},
|
||||
(value) => {
|
||||
console.log("access type changed:", value);
|
||||
onTypeChanged(value);
|
||||
}
|
||||
);
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(() => {
|
||||
onTypeChanged(props.type);
|
||||
});
|
||||
|
||||
return {
|
||||
crudBinding,
|
||||
crudRef
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.page-cert-access {
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div class="pi-access-selector">
|
||||
<span v-if="target.name" class="mlr-5">{{ target.name }}</span>
|
||||
<span v-else class="mlr-5 gray">请选择</span>
|
||||
<a-button @click="chooseForm.open">选择</a-button>
|
||||
<a-form-item-rest v-if="chooseForm.show">
|
||||
<a-modal v-model:visible="chooseForm.show" title="选择授权提供者" width="700px" @ok="chooseForm.ok">
|
||||
<div style="height: 400px; position: relative">
|
||||
<cert-access-modal v-model="selectedId" :type="type"></cert-access-modal>
|
||||
</div>
|
||||
</a-modal>
|
||||
</a-form-item-rest>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, reactive, ref, watch } from "vue";
|
||||
import * as api from "../api";
|
||||
import CertAccessModal from "./access/index.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "PiAccessSelector",
|
||||
components: { CertAccessModal },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "aliyun"
|
||||
}
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, ctx) {
|
||||
const target = ref({});
|
||||
const selectedId = ref();
|
||||
async function refreshTarget(value) {
|
||||
selectedId.value = value;
|
||||
if (value > 0) {
|
||||
target.value = await api.GetObj(value);
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => {
|
||||
return props.modelValue;
|
||||
},
|
||||
async (value) => {
|
||||
selectedId.value = null;
|
||||
target.value = {};
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
await refreshTarget(value);
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
const providerDefine = ref({});
|
||||
|
||||
async function refreshProviderDefine(type) {
|
||||
providerDefine.value = await api.GetProviderDefine(type);
|
||||
}
|
||||
watch(
|
||||
() => {
|
||||
return props.type;
|
||||
},
|
||||
async (value) => {
|
||||
await refreshProviderDefine(value);
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
const chooseForm = reactive({
|
||||
show: false,
|
||||
open() {
|
||||
chooseForm.show = true;
|
||||
},
|
||||
ok: () => {
|
||||
chooseForm.show = false;
|
||||
console.log("choose ok:", selectedId.value);
|
||||
refreshTarget(selectedId.value);
|
||||
ctx.emit("update:modelValue", selectedId.value);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
target,
|
||||
selectedId,
|
||||
providerDefine,
|
||||
chooseForm
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.access-selector {
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { request } from "/src/api/service";
|
||||
const apiPrefix = "/pi/access";
|
||||
export function GetList(query) {
|
||||
return request({
|
||||
url: apiPrefix + "/page",
|
||||
method: "post",
|
||||
data: query
|
||||
});
|
||||
}
|
||||
|
||||
export function AddObj(obj) {
|
||||
return request({
|
||||
url: apiPrefix + "/add",
|
||||
method: "post",
|
||||
data: obj
|
||||
});
|
||||
}
|
||||
|
||||
export function UpdateObj(obj) {
|
||||
return request({
|
||||
url: apiPrefix + "/update",
|
||||
method: "post",
|
||||
data: obj
|
||||
});
|
||||
}
|
||||
|
||||
export function DelObj(id) {
|
||||
return request({
|
||||
url: apiPrefix + "/delete",
|
||||
method: "post",
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
|
||||
export function GetObj(id) {
|
||||
return request({
|
||||
url: apiPrefix + "/info",
|
||||
method: "post",
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
|
||||
export function GetProviderDefine(type) {
|
||||
return request({
|
||||
url: apiPrefix + "/define",
|
||||
method: "post",
|
||||
params: { type }
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { dict } from "@fast-crud/fast-crud";
|
||||
import * as api from "./api";
|
||||
import _ from "lodash-es";
|
||||
|
||||
export function getCommonColumnDefine(crudBinding, typeRef) {
|
||||
const AccessTypeDictRef = dict({
|
||||
url: "/pi/access/accessTypeDict"
|
||||
});
|
||||
const defaultPluginConfig = {
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value"
|
||||
}
|
||||
};
|
||||
|
||||
function buildDefineFields(define, mode) {
|
||||
const columns = crudBinding.value[mode + "Form"].columns;
|
||||
for (const key in columns) {
|
||||
if (key.indexOf(".") >= 0) {
|
||||
delete columns[key];
|
||||
}
|
||||
}
|
||||
console.log('crudBinding.value[mode + "Form"].columns', columns);
|
||||
_.forEach(define.input, (value, mapKey) => {
|
||||
const key = "access." + mapKey;
|
||||
const field = {
|
||||
...value,
|
||||
key
|
||||
};
|
||||
columns[key] = _.merge({ title: key }, defaultPluginConfig, field);
|
||||
console.log("form", crudBinding.value[mode + "Form"]);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: {
|
||||
title: "类型",
|
||||
type: "dict-select",
|
||||
dict: AccessTypeDictRef,
|
||||
search: {
|
||||
show: false
|
||||
},
|
||||
form: {
|
||||
component: {
|
||||
disabled: false
|
||||
},
|
||||
rules: [{ required: true, message: "请选择类型" }],
|
||||
valueChange: {
|
||||
immediate: true,
|
||||
async handle({ value, mode, form }) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
const define = await api.GetProviderDefine(value);
|
||||
console.log("define", define);
|
||||
buildDefineFields(define, mode);
|
||||
}
|
||||
}
|
||||
},
|
||||
addForm: {
|
||||
value: typeRef
|
||||
}
|
||||
},
|
||||
setting: {
|
||||
column: { show: false },
|
||||
form: {
|
||||
show: false,
|
||||
valueBuilder({ value, form }) {
|
||||
form.access = {};
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const setting = JSON.parse(value);
|
||||
for (const key in setting) {
|
||||
form.access[key] = setting[key];
|
||||
}
|
||||
},
|
||||
valueResolve({ form }) {
|
||||
const setting = form.access;
|
||||
form.setting = JSON.stringify(setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import * as api from "./api";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ref } from "vue";
|
||||
import { getCommonColumnDefine } from "/@/views/certd/access/common";
|
||||
|
||||
export default function ({ expose }) {
|
||||
const { t } = useI18n();
|
||||
const pageRequest = async (query) => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const editRequest = async ({ form, row }) => {
|
||||
form.id = row.id;
|
||||
return await api.UpdateObj(form);
|
||||
};
|
||||
const delRequest = async ({ row }) => {
|
||||
return await api.DelObj(row.id);
|
||||
};
|
||||
|
||||
const addRequest = async ({ form }) => {
|
||||
return await api.AddObj(form);
|
||||
};
|
||||
const typeRef = ref();
|
||||
const { crudBinding } = expose;
|
||||
const commonColumnsDefine = getCommonColumnDefine(crudBinding, typeRef);
|
||||
return {
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest
|
||||
},
|
||||
form: {
|
||||
labelCol: {
|
||||
span: 6
|
||||
}
|
||||
},
|
||||
columns: {
|
||||
id: {
|
||||
title: "ID",
|
||||
key: "id",
|
||||
type: "number",
|
||||
column: {
|
||||
width: 50
|
||||
},
|
||||
form: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
name: {
|
||||
title: "名称",
|
||||
type: "text",
|
||||
form: {
|
||||
rules: [{ required: true, message: "必填项" }]
|
||||
}
|
||||
},
|
||||
...commonColumnsDefine
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<fs-page>
|
||||
<template #header>
|
||||
<div class="title">授权管理</div>
|
||||
</template>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted } from "vue";
|
||||
import { useCrud } from "@fast-crud/fast-crud";
|
||||
import createCrudOptions from "./crud";
|
||||
import { useExpose } from "@fast-crud/fast-crud";
|
||||
import { message } from "ant-design-vue";
|
||||
export default defineComponent({
|
||||
name: "CertdAccess",
|
||||
setup() {
|
||||
// crud组件的ref
|
||||
const crudRef = ref();
|
||||
// crud 配置的ref
|
||||
const crudBinding = ref();
|
||||
// 暴露的方法
|
||||
const { expose } = useExpose({ crudRef, crudBinding });
|
||||
// 你的crud配置
|
||||
const { crudOptions } = createCrudOptions({ expose });
|
||||
// 初始化crud配置
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
const { resetCrudOptions } = useCrud({ expose, crudOptions });
|
||||
// 你可以调用此方法,重新初始化crud配置
|
||||
// resetCrudOptions(options)
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(() => {
|
||||
expose.doRefresh();
|
||||
});
|
||||
|
||||
return {
|
||||
crudBinding,
|
||||
crudRef
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
import { request } from "/src/api/service";
|
||||
import { RunHistory } from "/@/views/certd/pipeline/pipeline/type";
|
||||
|
||||
const apiPrefix = "/pi/history";
|
||||
|
||||
export async function GetList(query) {
|
||||
const list = await request({
|
||||
url: apiPrefix + "/list",
|
||||
method: "post",
|
||||
data: query
|
||||
});
|
||||
for (const item of list) {
|
||||
if (item.pipeline) {
|
||||
item.pipeline = JSON.parse(item.pipeline);
|
||||
}
|
||||
}
|
||||
console.log("history", list);
|
||||
return list;
|
||||
}
|
||||
|
||||
export async function GetDetail(query): Promise<RunHistory> {
|
||||
const detail = await request({
|
||||
url: apiPrefix + "/detail",
|
||||
method: "post",
|
||||
params: query
|
||||
});
|
||||
|
||||
const pipeline = JSON.parse(detail.history?.pipeline || "{}");
|
||||
const logs = JSON.parse(detail.log?.logs || "{}");
|
||||
return {
|
||||
id: detail.history.id,
|
||||
pipeline,
|
||||
logs
|
||||
} as RunHistory;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { request } from "/src/api/service";
|
||||
import _ from "lodash-es";
|
||||
const apiPrefix = "/pi/plugin";
|
||||
|
||||
const defaultInputDefine = {
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "modelValue"
|
||||
}
|
||||
};
|
||||
|
||||
export async function GetList(query) {
|
||||
const plugins = await request({
|
||||
url: apiPrefix + "/list",
|
||||
method: "post",
|
||||
params: query
|
||||
});
|
||||
for (const plugin of plugins) {
|
||||
for (const key in plugin.input) {
|
||||
const field = _.merge({}, defaultInputDefine, plugin.input[key]);
|
||||
if (field.component.name === "a-input" || field.component.name === "a-select") {
|
||||
field.component.vModel = "value";
|
||||
}
|
||||
//嵌套对象
|
||||
field.key = ["input", key];
|
||||
if (field.required) {
|
||||
delete field.required;
|
||||
if (field.rules == null) {
|
||||
field.rules = [];
|
||||
}
|
||||
field.rules.push({ required: true, message: "此项必填" });
|
||||
}
|
||||
plugin.input[key] = field;
|
||||
}
|
||||
}
|
||||
console.log("plugins", plugins);
|
||||
return plugins;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { request } from "/src/api/service";
|
||||
const apiPrefix = "/pi/pipeline";
|
||||
|
||||
export function GetList(query) {
|
||||
return request({
|
||||
url: apiPrefix + "/page",
|
||||
method: "post",
|
||||
data: query
|
||||
});
|
||||
}
|
||||
|
||||
export function AddObj(obj) {
|
||||
return request({
|
||||
url: apiPrefix + "/add",
|
||||
method: "post",
|
||||
data: obj
|
||||
});
|
||||
}
|
||||
|
||||
export function UpdateObj(obj) {
|
||||
return request({
|
||||
url: apiPrefix + "/update",
|
||||
method: "post",
|
||||
data: obj
|
||||
});
|
||||
}
|
||||
|
||||
export function DelObj(id) {
|
||||
return request({
|
||||
url: apiPrefix + "/delete",
|
||||
method: "post",
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
|
||||
export function GetObj(id) {
|
||||
return request({
|
||||
url: apiPrefix + "/info",
|
||||
method: "post",
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
|
||||
export function GetDetail(id) {
|
||||
return request({
|
||||
url: apiPrefix + "/detail",
|
||||
method: "post",
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
|
||||
export function Save(pipelineEntity) {
|
||||
return request({
|
||||
url: apiPrefix + "/save",
|
||||
method: "post",
|
||||
data: pipelineEntity
|
||||
});
|
||||
}
|
||||
|
||||
export function Trigger(id) {
|
||||
return request({
|
||||
url: apiPrefix + "/trigger",
|
||||
method: "post",
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { compute } from "@fast-crud/fast-crud";
|
||||
import { Dicts } from "./dicts";
|
||||
|
||||
export default function () {
|
||||
return {
|
||||
crudOptions: {
|
||||
form: {
|
||||
wrapper: {
|
||||
width: "1150px"
|
||||
}
|
||||
},
|
||||
columns: {
|
||||
domains: {
|
||||
title: "域名",
|
||||
type: ["dict-select"],
|
||||
search: {
|
||||
show: true,
|
||||
component: {
|
||||
name: "a-input"
|
||||
}
|
||||
},
|
||||
form: {
|
||||
col: {
|
||||
span: 24
|
||||
},
|
||||
wrapperCol: {
|
||||
span: null
|
||||
},
|
||||
component: {
|
||||
mode: "tags",
|
||||
open: false
|
||||
},
|
||||
helper: {
|
||||
render: () => {
|
||||
return (
|
||||
<div>
|
||||
<div>支持通配符域名,例如: *.foo.com 、 *.test.handsfree.work</div>
|
||||
<div>支持多个域名、多个子域名、多个通配符域名打到一个证书上(域名必须是在同一个DNS提供商解析)</div>
|
||||
<div>多级子域名要分成多个域名输入(*.foo.com的证书不能用于xxx.yyy.foo.com)</div>
|
||||
<div>输入一个回车之后,再输入下一个</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
valueResolve({ form }) {
|
||||
if (form.domains instanceof String) {
|
||||
form.domains = form.domains?.join(",");
|
||||
}
|
||||
},
|
||||
rules: [{ required: true, message: "请填写域名" }]
|
||||
}
|
||||
},
|
||||
email: {
|
||||
title: "邮箱",
|
||||
type: "text",
|
||||
search: { show: false },
|
||||
form: {
|
||||
rules: [{ required: true, type: "email", message: "请填写邮箱" }]
|
||||
}
|
||||
},
|
||||
dnsProviderType: {
|
||||
title: "DNS提供商",
|
||||
type: "dict-select",
|
||||
dict: Dicts.dnsProviderTypeDict,
|
||||
form: {
|
||||
value: "aliyun",
|
||||
rules: [{ required: true, message: "请选择DNS提供商" }],
|
||||
valueChange({ form }) {
|
||||
form.dnsProviderAccess = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
dnsProviderAccess: {
|
||||
title: "DNS授权",
|
||||
type: "text",
|
||||
form: {
|
||||
component: {
|
||||
name: "PiAccessSelector",
|
||||
type: compute(({ form }) => {
|
||||
return form.dnsProviderType;
|
||||
}),
|
||||
vModel: "modelValue"
|
||||
},
|
||||
rules: [{ required: true, message: "请选择DNS授权" }]
|
||||
}
|
||||
}
|
||||
// country: {
|
||||
// title: "国家",
|
||||
// type: "text",
|
||||
// form: {
|
||||
// value: "China"
|
||||
// }
|
||||
// },
|
||||
// state: {
|
||||
// title: "省份",
|
||||
// type: "text",
|
||||
// form: {
|
||||
// value: "GuangDong"
|
||||
// }
|
||||
// },
|
||||
// locality: {
|
||||
// title: "市区",
|
||||
// type: "text",
|
||||
// form: {
|
||||
// value: "NanShan"
|
||||
// }
|
||||
// },
|
||||
// organization: {
|
||||
// title: "单位",
|
||||
// type: "text",
|
||||
// form: {
|
||||
// value: "CertD"
|
||||
// }
|
||||
// },
|
||||
// organizationUnit: {
|
||||
// title: "部门",
|
||||
// type: "text",
|
||||
// form: {
|
||||
// value: "IT Dept"
|
||||
// }
|
||||
// },
|
||||
// remark: {
|
||||
// title: "备注",
|
||||
// type: "text"
|
||||
// }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { dict } from "@fast-crud/fast-crud";
|
||||
|
||||
export const Dicts = {
|
||||
certIssuerDict: dict({ data: [{ value: "letencrypt", label: "LetEncrypt" }] }),
|
||||
challengeTypeDict: dict({ data: [{ value: "dns", label: "DNS校验" }] }),
|
||||
dnsProviderTypeDict: dict({
|
||||
url: "pi/dnsProvider/dnsProviderTypeDict"
|
||||
})
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<fs-form-wrapper ref="formWrapperRef" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useColumns, useExpose } from "@fast-crud/fast-crud";
|
||||
import createCrudOptions from "./crud.jsx";
|
||||
import { ref } from "vue";
|
||||
import _ from "lodash-es";
|
||||
export default {
|
||||
name: "PiCertdForm",
|
||||
setup(props, ctx) {
|
||||
// 自定义表单配置
|
||||
const { buildFormOptions } = useColumns();
|
||||
//使用crudOptions结构来构建自定义表单配置
|
||||
let { crudOptions } = createCrudOptions();
|
||||
const doSubmitRef = ref();
|
||||
const formOptions = buildFormOptions(
|
||||
_.merge(crudOptions, {
|
||||
form: {
|
||||
doSubmit({ form }) {
|
||||
// 创建certd 的pipeline
|
||||
doSubmitRef.value({ form });
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const formWrapperRef = ref();
|
||||
const formWrapperOptions = ref();
|
||||
formWrapperOptions.value = formOptions;
|
||||
function open(doSubmit) {
|
||||
doSubmitRef.value = doSubmit;
|
||||
formWrapperRef.value.open(formWrapperOptions.value);
|
||||
}
|
||||
|
||||
return {
|
||||
formWrapperRef,
|
||||
open,
|
||||
formWrapperOptions
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,225 @@
|
||||
import * as api from "./api";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ref, shallowRef } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { dict } from "@fast-crud/fast-crud";
|
||||
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
|
||||
import { nanoid } from "nanoid";
|
||||
import { message } from "ant-design-vue";
|
||||
export default function ({ expose, certdFormRef }) {
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const lastResRef = ref();
|
||||
const pageRequest = async (query) => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const editRequest = async ({ form, row }) => {
|
||||
form.id = row.id;
|
||||
const res = await api.UpdateObj(form);
|
||||
lastResRef.value = res;
|
||||
return res;
|
||||
};
|
||||
const delRequest = async ({ row }) => {
|
||||
return await api.DelObj(row.id);
|
||||
};
|
||||
|
||||
const addRequest = async ({ form }) => {
|
||||
form.content = JSON.stringify({
|
||||
title: form.title
|
||||
});
|
||||
const res = await api.AddObj(form);
|
||||
lastResRef.value = res;
|
||||
return res;
|
||||
};
|
||||
function addCertdPipeline() {
|
||||
certdFormRef.value.open(async ({ form }) => {
|
||||
// 添加certd pipeline
|
||||
const pipeline = {
|
||||
title: form.domains[0] + "证书自动化",
|
||||
stages: [
|
||||
{
|
||||
id: nanoid(),
|
||||
title: "证书申请阶段",
|
||||
tasks: [
|
||||
{
|
||||
id: nanoid(),
|
||||
title: "证书申请任务",
|
||||
steps: [
|
||||
{
|
||||
id: nanoid(),
|
||||
title: "申请证书",
|
||||
input: {
|
||||
renewDays: 20,
|
||||
...form
|
||||
},
|
||||
strategy: {
|
||||
runStrategy: 0 // 正常执行
|
||||
},
|
||||
type: "CertApply"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const id = await api.Save({
|
||||
content: JSON.stringify(pipeline),
|
||||
keepHistoryCount: 30
|
||||
});
|
||||
message.success("创建成功,请添加证书部署任务");
|
||||
router.push({ path: "/certd/pipeline/detail", query: { id, editMode: "true" } });
|
||||
});
|
||||
}
|
||||
return {
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest
|
||||
},
|
||||
actionbar: {
|
||||
buttons: {
|
||||
add: {
|
||||
order: 5,
|
||||
text: "自定义流水线"
|
||||
},
|
||||
addCertd: {
|
||||
order: 1,
|
||||
text: "添加证书流水线",
|
||||
type: "primary",
|
||||
click() {
|
||||
addCertdPipeline();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
form: {
|
||||
afterSubmit({ form, res, mode }) {
|
||||
if (mode === "add") {
|
||||
router.push({ path: "/certd/pipeline/detail", query: { id: res.id, editMode: "true" } });
|
||||
}
|
||||
}
|
||||
},
|
||||
rowHandle: {
|
||||
buttons: {
|
||||
view: {
|
||||
click({ row }) {
|
||||
router.push({ path: "/certd/pipeline/detail", query: { id: row.id, editMode: "false" } });
|
||||
}
|
||||
},
|
||||
config: {
|
||||
order: 1,
|
||||
title: null,
|
||||
type: "link",
|
||||
icon: "ant-design:edit-outlined",
|
||||
click({ row }) {
|
||||
router.push({ path: "/certd/pipeline/detail", query: { id: row.id, editMode: "true" } });
|
||||
}
|
||||
},
|
||||
edit: {
|
||||
order: 2,
|
||||
icon: "ant-design:setting-outlined"
|
||||
},
|
||||
remove: {
|
||||
order: 5
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: {
|
||||
id: {
|
||||
title: "ID",
|
||||
key: "id",
|
||||
type: "number",
|
||||
column: {
|
||||
width: 50
|
||||
},
|
||||
form: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
title: {
|
||||
title: "流水线名称",
|
||||
type: "text",
|
||||
search: {
|
||||
show: true,
|
||||
component: {
|
||||
name: "a-input"
|
||||
}
|
||||
},
|
||||
column: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
lastHistoryTime: {
|
||||
title: "最后运行",
|
||||
type: "datetime",
|
||||
form: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
status: {
|
||||
title: "状态",
|
||||
type: "dict-select",
|
||||
dict: dict({
|
||||
data: statusUtil.getOptions()
|
||||
}),
|
||||
form: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
|
||||
disabled: {
|
||||
title: "启用",
|
||||
type: "dict-switch",
|
||||
dict: dict({
|
||||
data: [
|
||||
{ value: true, label: "禁用" },
|
||||
{ value: false, label: "启用" }
|
||||
]
|
||||
}),
|
||||
form: {
|
||||
value: false,
|
||||
show: false
|
||||
},
|
||||
column: {
|
||||
component: {
|
||||
name: "fs-dict-switch",
|
||||
vModel: "checked"
|
||||
},
|
||||
async valueChange({ row, key, value }) {
|
||||
return await api.UpdateObj({
|
||||
id: row.id,
|
||||
disabled: row[key]
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
keepHistoryCount: {
|
||||
title: "历史记录保持数",
|
||||
type: "number",
|
||||
form: {
|
||||
value: 30,
|
||||
helper: "历史记录保持条数,多余的会被删除"
|
||||
}
|
||||
},
|
||||
createTime: {
|
||||
title: "创建时间",
|
||||
type: "datetime",
|
||||
form: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
updateTime: {
|
||||
title: "更新时间",
|
||||
type: "datetime",
|
||||
form: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
https://stackoverflow.com/questions/28365839/dashed-border-animation-in-css3-animation
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<fs-page class="fs-pipeline-detail">
|
||||
<pipeline-edit v-model:edit-mode="editMode" :pipeline-id="pipelineId" :options="pipelineOptions"></pipeline-edit>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, Ref, ref } from "vue";
|
||||
import PipelineEdit from "./pipeline/index.vue";
|
||||
import * as pluginApi from "./api.plugin";
|
||||
import * as historyApi from "./api.history";
|
||||
import * as api from "./api";
|
||||
import { useRoute } from "vue-router";
|
||||
import { Pipeline, PipelineDetail, PipelineOptions, RunHistory } from "/@/views/certd/pipeline/pipeline/type";
|
||||
import { PluginDefine } from "@certd/pipeline/src";
|
||||
|
||||
export default defineComponent({
|
||||
name: "PipelineDetail",
|
||||
components: { PipelineEdit },
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const pipelineId = ref(route.query.id);
|
||||
|
||||
const getPipelineDetail = async ({ pipelineId }) => {
|
||||
const detail = await api.GetDetail(pipelineId);
|
||||
return {
|
||||
pipeline: {
|
||||
id: detail.pipeline.id,
|
||||
stages: [],
|
||||
triggers: [],
|
||||
...JSON.parse(detail.pipeline.content || "{}")
|
||||
}
|
||||
} as PipelineDetail;
|
||||
};
|
||||
|
||||
const getHistoryList = async ({ pipelineId }) => {
|
||||
const list: RunHistory[] = await historyApi.GetList({ pipelineId });
|
||||
return list;
|
||||
};
|
||||
|
||||
const getHistoryDetail = async ({ historyId }): Promise<RunHistory> => {
|
||||
const detail = await historyApi.GetDetail({ id: historyId });
|
||||
return detail;
|
||||
};
|
||||
|
||||
const getPlugins = async () => {
|
||||
const plugins = await pluginApi.GetList({});
|
||||
return plugins as PluginDefine[];
|
||||
};
|
||||
|
||||
async function doSave(pipelineConfig: Pipeline) {
|
||||
await api.Save({
|
||||
id: pipelineConfig.id,
|
||||
content: JSON.stringify(pipelineConfig)
|
||||
});
|
||||
}
|
||||
async function doTrigger({ pipelineId }) {
|
||||
await api.Trigger(pipelineId);
|
||||
}
|
||||
|
||||
const pipelineOptions: Ref<PipelineOptions> = ref({
|
||||
doTrigger,
|
||||
doSave,
|
||||
getPlugins,
|
||||
getHistoryList,
|
||||
getHistoryDetail,
|
||||
getPipelineDetail
|
||||
});
|
||||
|
||||
const editMode = ref(false);
|
||||
if (route.query.editMode !== "false") {
|
||||
editMode.value = true;
|
||||
}
|
||||
|
||||
return {
|
||||
pipelineOptions,
|
||||
pipelineId,
|
||||
editMode
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.page-pipeline-detail {
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<fs-page class="page-cert">
|
||||
<template #header>
|
||||
<div class="title">我的流水线</div>
|
||||
</template>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
<pi-certd-form ref="certdFormRef"></pi-certd-form>
|
||||
</fs-crud>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, onMounted } from "vue";
|
||||
import { useCrud } from "@fast-crud/fast-crud";
|
||||
import createCrudOptions from "./crud";
|
||||
import { useExpose } from "@fast-crud/fast-crud";
|
||||
import PiCertdForm from "./certd-form/index.vue";
|
||||
export default defineComponent({
|
||||
name: "PipelineManager",
|
||||
components: { PiCertdForm },
|
||||
setup() {
|
||||
const certdFormRef = ref();
|
||||
|
||||
// crud组件的ref
|
||||
const crudRef = ref();
|
||||
// crud 配置的ref
|
||||
const crudBinding = ref();
|
||||
|
||||
// 暴露的方法
|
||||
const { expose } = useExpose({ crudRef, crudBinding });
|
||||
// 你的crud配置
|
||||
const { crudOptions } = createCrudOptions({ expose, certdFormRef });
|
||||
// 初始化crud配置
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
const { resetCrudOptions } = useCrud({ expose, crudOptions });
|
||||
// 你可以调用此方法,重新初始化crud配置
|
||||
// resetCrudOptions(options)
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(() => {
|
||||
expose.doRefresh();
|
||||
});
|
||||
|
||||
return {
|
||||
crudBinding,
|
||||
crudRef,
|
||||
certdFormRef
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less"></style>
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<a-timeline-item v-if="status && runnable" class="pi-history-timeline-item" :color="status.color">
|
||||
<template #dot>
|
||||
<fs-icon v-bind="status" />
|
||||
</template>
|
||||
<p>
|
||||
<fs-date-format :model-value="runnable.status?.startTime"></fs-date-format>
|
||||
<a-tag class="ml-10" :color="status.color">{{ status.label }}</a-tag>
|
||||
|
||||
<a-tag v-if="isCurrent" class="pointer" color="green" :closable="true" @close="cancel">当前</a-tag>
|
||||
<a-tag v-else-if="!editMode" class="pointer" color="blue" @click="view">查看</a-tag>
|
||||
</p>
|
||||
</a-timeline-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, provide, Ref, watch, computed } from "vue";
|
||||
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
|
||||
export default defineComponent({
|
||||
name: "PiHistoryTimelineItem",
|
||||
props: {
|
||||
runnable: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "icon"
|
||||
},
|
||||
isCurrent: {
|
||||
type: Boolean
|
||||
},
|
||||
editMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ["view", "cancel"],
|
||||
setup(props, ctx) {
|
||||
const status = computed(() => {
|
||||
return statusUtil.get(props.runnable?.status?.result);
|
||||
});
|
||||
|
||||
function view() {
|
||||
ctx.emit("view");
|
||||
}
|
||||
function cancel() {
|
||||
ctx.emit("cancel");
|
||||
}
|
||||
return {
|
||||
status,
|
||||
cancel,
|
||||
view
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.pi-history-timeline-item {
|
||||
.ant-tag.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<a-select class="pi-output-selector" :value="modelValue" :options="options" @update:value="onChanged"> </a-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { inject, onMounted, Ref, ref, watch } from "vue";
|
||||
import { pluginManager } from "../../plugin";
|
||||
|
||||
export default {
|
||||
name: "PiOutputSelector",
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, ctx) {
|
||||
const options = ref<any[]>([]);
|
||||
|
||||
const pipeline = inject("pipeline") as Ref<any>;
|
||||
const currentStageIndex = inject("currentStageIndex") as Ref<number>;
|
||||
const currentStepIndex = inject("currentStepIndex") as Ref<number>;
|
||||
const currentTask = inject("currentTask") as Ref<any>;
|
||||
|
||||
function onCreate() {
|
||||
options.value = pluginManager.getPreStepOutputOptions({
|
||||
pipeline: pipeline.value,
|
||||
currentStageIndex: currentStageIndex.value,
|
||||
currentStepIndex: currentStepIndex.value,
|
||||
currentTask: currentTask.value
|
||||
});
|
||||
if (props.modelValue == null && options.value.length > 0) {
|
||||
ctx.emit("update:modelValue", options.value[0].value);
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
onCreate();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => {
|
||||
return pluginManager.map;
|
||||
},
|
||||
() => {
|
||||
onCreate();
|
||||
}
|
||||
);
|
||||
|
||||
function onChanged(value) {
|
||||
ctx.emit("update:modelValue", value);
|
||||
}
|
||||
return {
|
||||
options,
|
||||
onChanged
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<span v-if="statusRef" class="pi-status-show">
|
||||
<template v-if="type === 'icon'">
|
||||
<fs-icon class="status-icon" v-bind="statusRef" :style="{ color: statusRef.color }" />
|
||||
</template>
|
||||
<template v-if="type === 'tag'">
|
||||
<a-tag :color="statusRef.color">{{ statusRef.label }}</a-tag>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, provide, Ref, watch, computed } from "vue";
|
||||
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
|
||||
export default defineComponent({
|
||||
name: "PiStatusShow",
|
||||
props: {
|
||||
status: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "icon"
|
||||
}
|
||||
},
|
||||
setup(props, ctx) {
|
||||
const statusRef = computed(() => {
|
||||
return statusUtil.get(props.status);
|
||||
});
|
||||
|
||||
return {
|
||||
statusRef
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.pi-status-show {
|
||||
.status-icon {
|
||||
font-size: 16px;
|
||||
margin-left: 3px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="stepDrawerVisible" placement="right" :closable="true" width="600px" :after-visible-change="stepDrawerOnAfterVisibleChange">
|
||||
<template #title>
|
||||
编辑任务
|
||||
<a-button v-if="editMode" @click="stepDelete()">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-if="currentStep">
|
||||
<pi-container v-if="currentStep._isAdd" class="pi-step-form">
|
||||
<a-row :gutter="10">
|
||||
<a-col v-for="(item, index) of stepPluginDefineList" :key="index" class="step-plugin" :span="12">
|
||||
<a-card
|
||||
hoverable
|
||||
:class="{ current: item.name === currentStep.type }"
|
||||
@click="stepTypeSelected(item)"
|
||||
@dblclick="
|
||||
stepTypeSelected(item);
|
||||
stepTypeSave();
|
||||
"
|
||||
>
|
||||
<a-card-meta>
|
||||
<template #title>
|
||||
<a-avatar :src="item.icon || '/images/plugin.png'" />
|
||||
<span class="title">{{ item.title }}</span>
|
||||
</template>
|
||||
<template #description>
|
||||
<span :title="item.desc">{{ item.desc }}</span>
|
||||
</template>
|
||||
</a-card-meta>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-button v-if="editMode" type="primary" @click="stepTypeSave"> 确定 </a-button>
|
||||
</pi-container>
|
||||
<pi-container v-else class="pi-step-form">
|
||||
<a-form ref="stepFormRef" class="step-form" :model="currentStep" :label-col="labelCol" :wrapper-col="wrapperCol">
|
||||
<div class="mb-10">
|
||||
<a-alert type="info" :message="currentPlugin.title" :description="currentPlugin.desc"> </a-alert>
|
||||
</div>
|
||||
<fs-form-item
|
||||
v-model="currentStep.title"
|
||||
:item="{
|
||||
title: '任务名称',
|
||||
key: 'title',
|
||||
component: {
|
||||
name: 'a-input',
|
||||
vModel: 'value'
|
||||
},
|
||||
rules: [{ required: true, message: '此项必填' }]
|
||||
}"
|
||||
:get-context-fn="blankFn"
|
||||
/>
|
||||
<template v-for="(item, key) in currentPlugin.input" :key="key">
|
||||
<fs-form-item v-model="currentStep.input[key]" :item="item" :get-context-fn="blankFn" />
|
||||
</template>
|
||||
|
||||
<fs-form-item
|
||||
v-model="currentStep.strategy.runStrategy"
|
||||
:item="{
|
||||
title: '运行策略',
|
||||
key: 'strategy.runStrategy',
|
||||
component: {
|
||||
name: 'a-select',
|
||||
vModel: 'value',
|
||||
options: [
|
||||
{ value: 0, label: '正常运行' },
|
||||
{ value: 1, label: '成功后跳过' }
|
||||
]
|
||||
},
|
||||
rules: [{ required: true, message: '此项必填' }]
|
||||
}"
|
||||
:get-context-fn="blankFn"
|
||||
/>
|
||||
</a-form>
|
||||
|
||||
<template #footer>
|
||||
<a-form-item v-if="editMode" :wrapper-col="{ span: 14, offset: 4 }">
|
||||
<a-button type="primary" @click="stepSave"> 确定 </a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</pi-container>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="jsx">
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { inject, ref } from "vue";
|
||||
import _ from "lodash-es";
|
||||
import { nanoid } from "nanoid";
|
||||
export default {
|
||||
name: "PiStepForm",
|
||||
props: {
|
||||
editMode: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ["update"],
|
||||
setup(props, context) {
|
||||
/**
|
||||
* step drawer
|
||||
* @returns
|
||||
*/
|
||||
function useStepForm() {
|
||||
const stepPluginDefineList = inject("plugins");
|
||||
|
||||
const mode = ref("add");
|
||||
const callback = ref();
|
||||
const currentStep = ref({ title: undefined, input: {} });
|
||||
const currentPlugin = ref({});
|
||||
const stepFormRef = ref(null);
|
||||
const stepDrawerVisible = ref(false);
|
||||
const rules = ref({
|
||||
name: [
|
||||
{
|
||||
type: "string",
|
||||
required: true,
|
||||
message: "请输入名称"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const stepTypeSelected = (item) => {
|
||||
currentStep.value.type = item.name;
|
||||
currentStep.value.title = item.title;
|
||||
console.log("currentStepTypeChanged:", currentStep.value);
|
||||
};
|
||||
|
||||
const stepTypeSave = () => {
|
||||
currentStep.value._isAdd = false;
|
||||
if (currentStep.value.type == null) {
|
||||
message.warn("请先选择类型");
|
||||
return;
|
||||
}
|
||||
// 给step的input设置默认值
|
||||
changeCurrentPlugin(currentStep.value);
|
||||
|
||||
//赋初始值
|
||||
_.merge(currentStep.value, { input: {}, strategy: { runStrategy: 0 } }, currentPlugin.value.default, currentStep.value);
|
||||
|
||||
for (const key in currentPlugin.value.input) {
|
||||
const input = currentPlugin.value.input[key];
|
||||
if (input.default != null) {
|
||||
currentStep.value.input[key] = input.default ?? input.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stepDrawerShow = () => {
|
||||
stepDrawerVisible.value = true;
|
||||
};
|
||||
const stepDrawerClose = () => {
|
||||
stepDrawerVisible.value = false;
|
||||
};
|
||||
|
||||
const stepDrawerOnAfterVisibleChange = (val) => {
|
||||
console.log("stepDrawerOnAfterVisibleChange", val);
|
||||
};
|
||||
|
||||
const stepOpen = (step, emit) => {
|
||||
callback.value = emit;
|
||||
currentStep.value = _.merge({ input: {}, strategy: {} }, step);
|
||||
console.log("currentStepOpen", currentStep.value);
|
||||
if (step.type) {
|
||||
changeCurrentPlugin(currentStep.value);
|
||||
}
|
||||
stepDrawerShow();
|
||||
};
|
||||
|
||||
const stepAdd = (emit) => {
|
||||
mode.value = "add";
|
||||
const step = {
|
||||
id: nanoid(),
|
||||
title: "新任务",
|
||||
type: undefined,
|
||||
_isAdd: true,
|
||||
input: {},
|
||||
status: null
|
||||
};
|
||||
stepOpen(step, emit);
|
||||
};
|
||||
|
||||
const stepEdit = (step, emit) => {
|
||||
mode.value = "edit";
|
||||
stepOpen(step, emit);
|
||||
};
|
||||
|
||||
const stepView = (step, emit) => {
|
||||
mode.value = "view";
|
||||
stepOpen(step, emit);
|
||||
};
|
||||
|
||||
const changeCurrentPlugin = (step) => {
|
||||
const stepType = step.type;
|
||||
const pluginDefine = stepPluginDefineList.value.find((p) => {
|
||||
return p.name === stepType;
|
||||
});
|
||||
if (pluginDefine) {
|
||||
step.type = stepType;
|
||||
step._isAdd = false;
|
||||
currentPlugin.value = pluginDefine;
|
||||
}
|
||||
console.log("currentStepTypeChanged:", currentStep.value);
|
||||
console.log("currentStepPlugin:", currentPlugin.value);
|
||||
};
|
||||
|
||||
const stepSave = async (e) => {
|
||||
console.log("currentStepSave", currentStep.value);
|
||||
try {
|
||||
await stepFormRef.value.validate();
|
||||
} catch (e) {
|
||||
console.error("表单验证失败:", e);
|
||||
return;
|
||||
}
|
||||
|
||||
callback.value("save", currentStep.value);
|
||||
stepDrawerClose();
|
||||
};
|
||||
|
||||
const stepDelete = () => {
|
||||
Modal.confirm({
|
||||
title: "确认",
|
||||
content: `确定要删除此步骤吗?`,
|
||||
async onOk() {
|
||||
callback.value("delete");
|
||||
stepDrawerClose();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const blankFn = () => {
|
||||
return {};
|
||||
};
|
||||
return {
|
||||
stepTypeSelected,
|
||||
stepTypeSave,
|
||||
stepPluginDefineList,
|
||||
stepFormRef,
|
||||
mode,
|
||||
stepAdd,
|
||||
stepEdit,
|
||||
stepView,
|
||||
stepDrawerShow,
|
||||
stepDrawerVisible,
|
||||
stepDrawerOnAfterVisibleChange,
|
||||
currentStep,
|
||||
currentPlugin,
|
||||
stepSave,
|
||||
stepDelete,
|
||||
rules,
|
||||
blankFn
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...useStepForm(),
|
||||
labelCol: { span: 6 },
|
||||
wrapperCol: { span: 16 }
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.pi-step-form {
|
||||
.body {
|
||||
padding: 10px;
|
||||
.ant-card {
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.current {
|
||||
border-color: #00b7ff;
|
||||
}
|
||||
|
||||
.ant-card-meta-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.ant-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-left: 5px;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 14px;
|
||||
height: 100px;
|
||||
|
||||
overflow-y: hidden;
|
||||
|
||||
.ant-card-meta-description {
|
||||
font-size: 10px;
|
||||
line-height: 20px;
|
||||
height: 40px;
|
||||
color: #7f7f7f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
v-model:visible="taskDrawerVisible"
|
||||
placement="right"
|
||||
:closable="true"
|
||||
width="600px"
|
||||
class="pi-task-form"
|
||||
:after-visible-change="taskDrawerOnAfterVisibleChange"
|
||||
>
|
||||
<template #title>
|
||||
编辑任务
|
||||
<a-button v-if="editMode" @click="taskDelete()">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-if="currentTask">
|
||||
<pi-container>
|
||||
<a-form
|
||||
ref="taskFormRef"
|
||||
class="task-form"
|
||||
:model="currentTask"
|
||||
:label-col="labelCol"
|
||||
:wrapper-col="wrapperCol"
|
||||
>
|
||||
<fs-form-item
|
||||
v-model="currentTask.title"
|
||||
:item="{
|
||||
title: '任务名称',
|
||||
key: 'title',
|
||||
component: {
|
||||
name: 'a-input',
|
||||
vModel: 'value'
|
||||
},
|
||||
rules: [{ required: true, message: '此项必填' }]
|
||||
}"
|
||||
:get-context-fn="blankFn"
|
||||
/>
|
||||
|
||||
<div class="steps">
|
||||
<a-form-item
|
||||
:value="currentTask.steps"
|
||||
name="steps"
|
||||
label=""
|
||||
:wrapper-col="{ span: 24 }"
|
||||
:rules="[{ required: true, message: '至少需要一个步骤,或者你可以点击标题右边删除按钮删除此任务' }]"
|
||||
>
|
||||
<a-descriptions title="任务步骤" size="small">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="stepAdd(currentTask)">添加步骤</a-button>
|
||||
</template>
|
||||
</a-descriptions>
|
||||
<a-list class="step-list" item-layout="horizontal" :data-source="currentTask.steps">
|
||||
<template #renderItem="{ item, index }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<a key="edit" @click="stepEdit(currentTask, item, index)">编辑</a>
|
||||
<a key="remove" @click="stepDelete(currentTask, index)">删除</a>
|
||||
</template>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
{{ item.title }}
|
||||
</template>
|
||||
<template #avatar>
|
||||
<fs-icon icon="ion:flash"></fs-icon>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
|
||||
<pi-step-form ref="stepFormRef" :edit-mode="editMode"></pi-step-form>
|
||||
|
||||
<template #footer>
|
||||
<a-form-item v-if="editMode" :wrapper-col="{ span: 14, offset: 4 }">
|
||||
<a-button type="primary" @click="taskSave"> 确定 </a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</pi-container>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { provide, Ref, ref } from "vue";
|
||||
import _ from "lodash-es";
|
||||
import { nanoid } from "nanoid";
|
||||
import PiStepForm from "../step-form/index.vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
|
||||
export default {
|
||||
name: "PiTaskForm",
|
||||
components: { PiStepForm },
|
||||
props: {
|
||||
editMode: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ["update"],
|
||||
setup(props, ctx) {
|
||||
function useStep() {
|
||||
const stepFormRef: Ref<any> = ref(null);
|
||||
const currentStepIndex = ref(0);
|
||||
provide("currentStepIndex", currentStepIndex);
|
||||
const stepAdd = (task) => {
|
||||
currentStepIndex.value = task.steps.length;
|
||||
stepFormRef.value.stepAdd((type, value) => {
|
||||
if (type === "save") {
|
||||
task.steps.push(value);
|
||||
if (!task.title || task.title === "新任务") {
|
||||
task.title = value.title;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
const stepEdit = (task, step, stepIndex) => {
|
||||
currentStepIndex.value = stepIndex;
|
||||
console.log("step.edit start", task, step, props.editMode);
|
||||
if (props.editMode) {
|
||||
console.log("step.edit", task, step);
|
||||
stepFormRef.value.stepEdit(step, (type, value) => {
|
||||
console.log("step.save", step, type, value);
|
||||
if (type === "delete") {
|
||||
task.steps.splice(stepIndex, 1);
|
||||
} else if (type === "save") {
|
||||
task.steps[stepIndex] = { ...value };
|
||||
}
|
||||
console.log("task.steps", task.steps);
|
||||
});
|
||||
} else {
|
||||
stepFormRef.value.stepView(step, (type, value) => {});
|
||||
}
|
||||
};
|
||||
|
||||
const stepDelete = (task, stepIndex) => {
|
||||
Modal.confirm({
|
||||
title: "确认",
|
||||
content: `确定要删除此步骤吗?`,
|
||||
async onOk() {
|
||||
task.steps.splice(stepIndex, 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return { stepAdd, stepEdit, stepDelete, stepFormRef };
|
||||
}
|
||||
|
||||
/**
|
||||
* task drawer
|
||||
* @returns
|
||||
*/
|
||||
function useTaskForm() {
|
||||
const mode = ref("add");
|
||||
const callback = ref();
|
||||
const currentTask = ref({ title: undefined, steps: [] });
|
||||
provide("currentTask", currentTask);
|
||||
const taskFormRef: Ref<any> = ref(null);
|
||||
const taskDrawerVisible = ref(false);
|
||||
const rules = ref({
|
||||
name: [
|
||||
{
|
||||
type: "string",
|
||||
required: true,
|
||||
message: "请输入名称"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const taskDrawerShow = () => {
|
||||
taskDrawerVisible.value = true;
|
||||
};
|
||||
const taskDrawerClose = () => {
|
||||
taskDrawerVisible.value = false;
|
||||
};
|
||||
|
||||
const taskDrawerOnAfterVisibleChange = (val) => {
|
||||
console.log("taskDrawerOnAfterVisibleChange", val);
|
||||
};
|
||||
|
||||
const taskOpen = (task, emit) => {
|
||||
callback.value = emit;
|
||||
currentTask.value = _.merge({ steps: {} }, task);
|
||||
console.log("currentTaskOpen", currentTask.value);
|
||||
taskDrawerShow();
|
||||
};
|
||||
|
||||
const taskAdd = (emit) => {
|
||||
mode.value = "add";
|
||||
const task = { id: nanoid(), title: "新任务", steps: [], status: null };
|
||||
taskOpen(task, emit);
|
||||
};
|
||||
|
||||
const taskEdit = (task, emit) => {
|
||||
mode.value = "edit";
|
||||
taskOpen(task, emit);
|
||||
};
|
||||
|
||||
const taskView = (task, emit) => {
|
||||
mode.value = "view";
|
||||
taskOpen(task, emit);
|
||||
};
|
||||
|
||||
const taskSave = async (e) => {
|
||||
console.log("currentTaskSave", currentTask.value);
|
||||
try {
|
||||
await taskFormRef.value.validate();
|
||||
} catch (e) {
|
||||
console.error("表单验证失败:", e);
|
||||
return;
|
||||
}
|
||||
|
||||
callback.value("save", currentTask.value);
|
||||
taskDrawerClose();
|
||||
};
|
||||
|
||||
const taskDelete = () => {
|
||||
Modal.confirm({
|
||||
title: "确认",
|
||||
content: `确定要删除此任务吗?`,
|
||||
async onOk() {
|
||||
callback.value("delete");
|
||||
taskDrawerClose();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const blankFn = () => {
|
||||
return {};
|
||||
};
|
||||
return {
|
||||
taskFormRef,
|
||||
mode,
|
||||
taskAdd,
|
||||
taskEdit,
|
||||
taskView,
|
||||
taskDrawerShow,
|
||||
taskDrawerVisible,
|
||||
taskDrawerOnAfterVisibleChange,
|
||||
currentTask,
|
||||
taskSave,
|
||||
taskDelete,
|
||||
rules,
|
||||
blankFn
|
||||
};
|
||||
}
|
||||
return {
|
||||
labelCol: { span: 6 },
|
||||
wrapperCol: { span: 16 },
|
||||
...useTaskForm(),
|
||||
...useStep()
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.pi-task-form {
|
||||
.steps {
|
||||
margin: 0 50px 0 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="taskModal.visible"
|
||||
class="pi-task-view"
|
||||
title="任务日志"
|
||||
style="width: 80%"
|
||||
v-bind="taskModal"
|
||||
>
|
||||
<a-tabs v-model:activeKey="activeKey" tab-position="left" animated>
|
||||
<a-tab-pane v-for="item of detail.nodes" :key="item.node.id">
|
||||
<template #tab>
|
||||
<div class="tab-title" :title="item.node.title">
|
||||
<span class="tab-title-text">【{{ item.type }}】 {{ item.node.title }}</span>
|
||||
<pi-status-show :status="item.node.status?.result" type="icon"></pi-status-show>
|
||||
</div>
|
||||
</template>
|
||||
<pre
|
||||
class="pi-task-view-logs"
|
||||
><template v-for="(text, index) of item.logs" :key="index">{{ text }}</template></pre>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { inject, provide, Ref, ref } from "vue";
|
||||
import { RunHistory } from "/@/views/certd/pipeline/pipeline/type";
|
||||
import PiStatusShow from "/@/views/certd/pipeline/pipeline/component/status-show.vue";
|
||||
|
||||
export default {
|
||||
name: "PiTaskView",
|
||||
components: { PiStatusShow },
|
||||
props: {},
|
||||
setup(props, ctx) {
|
||||
const taskModal = ref({
|
||||
visible: false,
|
||||
onOk() {
|
||||
taskViewClose();
|
||||
},
|
||||
cancelText: "关闭"
|
||||
});
|
||||
|
||||
const detail = ref({ nodes: [] });
|
||||
const activeKey = ref();
|
||||
const currentHistory: Ref<RunHistory> | undefined = inject("currentHistory");
|
||||
const taskViewOpen = (task) => {
|
||||
taskModal.value.visible = true;
|
||||
const nodes: any = [];
|
||||
// nodes.push({
|
||||
// node: task,
|
||||
// type: "任务",
|
||||
// tab: 0,
|
||||
// logs: [],
|
||||
// result: {}
|
||||
// });
|
||||
for (let step of task.steps) {
|
||||
nodes.push({
|
||||
node: step,
|
||||
type: "步骤",
|
||||
tab: 2,
|
||||
logs: []
|
||||
});
|
||||
}
|
||||
for (let node of nodes) {
|
||||
if (currentHistory?.value?.logs != null) {
|
||||
node.logs = currentHistory.value.logs[node.node.id] || [];
|
||||
}
|
||||
}
|
||||
|
||||
if (task.steps.length > 0) {
|
||||
activeKey.value = task.steps[0].id;
|
||||
}
|
||||
|
||||
detail.value = { nodes };
|
||||
|
||||
console.log("nodes", nodes);
|
||||
};
|
||||
|
||||
const taskViewClose = () => {
|
||||
taskModal.value.visible = false;
|
||||
};
|
||||
|
||||
return {
|
||||
detail,
|
||||
taskModal,
|
||||
activeKey,
|
||||
taskViewOpen,
|
||||
taskViewClose
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.pi-task-view {
|
||||
.tab-title {
|
||||
display: flex;
|
||||
.tab-title-text {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.pi-task-view-logs {
|
||||
background-color: #000c17;
|
||||
color: #fafafa;
|
||||
min-height: 300px;
|
||||
max-height: 580px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user