build: trident-sync prepare

This commit is contained in:
xiaojunnuo
2023-01-29 13:44:19 +08:00
parent dcd1023a39
commit 07a45b4530
589 changed files with 36886 additions and 2 deletions
+56
View File
@@ -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"
});
}
+138
View File
@@ -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 });
+66
View File
@@ -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: "确定要注销登录吗?"
}
}
};
@@ -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 &copy; 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>
+23
View File
@@ -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");
+280
View File
@@ -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 contextuseCrud的参数
*/
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
View File
@@ -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);
}
+1
View File
@@ -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
View File
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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