feat: 站点个性化设置

pull/213/head
xiaojunnuo 2024-10-05 01:46:25 +08:00
parent ce9a9862f1
commit 11a9fe9014
57 changed files with 710 additions and 763 deletions

View File

@ -27,7 +27,8 @@
"node-forge": "^1.3.1",
"nodemailer": "^6.9.3",
"proxy-agent": "^6.4.0",
"qs": "^6.11.2"
"qs": "^6.11.2",
"dayjs": "^1.11.7"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.4",

View File

@ -16,6 +16,8 @@ import { promises } from "./util.promise.js";
import { fileUtils } from "./util.file.js";
import _ from "lodash-es";
import { cache } from "./util.cache.js";
import dayjs from 'dayjs';
export const utils = {
sleep,
http,
@ -27,4 +29,5 @@ export const utils = {
mergeUtils,
cache,
nanoid,
dayjs
};

View File

@ -25,7 +25,26 @@ export function safePromise<T>(callback: (resolve: (ret: T) => void, reject: (re
});
}
export function promisify(func: any) {
return function (...args: any) {
return new Promise((resolve, reject) => {
try {
func(...args, (err: any, data: any) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
} catch (e) {
reject(e);
}
});
};
}
export const promises = {
TimeoutPromise,
safePromise,
promisify,
};

View File

@ -35,7 +35,9 @@
"@midwayjs/cache": "^3",
"better-sqlite3": "^11.1.2",
"typeorm": "^0.3.20",
"lodash-es": "^4.17.21"
"lodash-es": "^4.17.21",
"dayjs": "^1.11.7",
"@midwayjs/upload": "3"
},
"devDependencies": {
"mwts": "^1.3.0",

View File

@ -44,6 +44,14 @@ export const Constants = {
code: 402,
message: '您没有权限',
},
param: {
code: 400,
message: '参数错误',
},
notFound: {
code: 404,
message: '页面/文件/资源不存在',
},
preview: {
code: 10001,
message: '对不起,预览环境不允许修改此数据',

View File

@ -1,7 +1,9 @@
export * from './auth-exception.js'
export * from './base-exception.js'
export * from './permission-exception.js'
export * from './preview-exception.js'
export * from './validation-exception.js'
export * from './vip-exception.js'
export * from './common-exception.js'
export * from './auth-exception.js';
export * from './base-exception.js';
export * from './permission-exception.js';
export * from './preview-exception.js';
export * from './validation-exception.js';
export * from './vip-exception.js';
export * from './common-exception.js';
export * from './not-found-exception.js';
export * from './param-exception.js';

View File

@ -0,0 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
/**
*
*/
export class NotFoundException extends BaseException {
constructor(message) {
super('NotFoundException', Constants.res.notFound.code, message ? message : Constants.res.notFound.message);
}
}

View File

@ -0,0 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
/**
*
*/
export class ParamException extends BaseException {
constructor(message) {
super('ParamException', Constants.res.param.code, message ? message : Constants.res.param.message);
}
}

View File

@ -5,10 +5,6 @@ import { BaseException } from './base-exception.js';
*/
export class PermissionException extends BaseException {
constructor(message?: string) {
super(
'PermissionException',
Constants.res.permission.code,
message ? message : Constants.res.permission.message
);
super('PermissionException', Constants.res.permission.code, message ? message : Constants.res.permission.message);
}
}

View File

@ -5,10 +5,6 @@ import { BaseException } from './base-exception.js';
*/
export class ValidateException extends BaseException {
constructor(message) {
super(
'ValidateException',
Constants.res.validation.code,
message ? message : Constants.res.validation.message
);
super('ValidateException', Constants.res.validation.code, message ? message : Constants.res.validation.message);
}
}

View File

@ -1 +1,2 @@
export * from './service/plus-service.js';
export * from './service/file-service.js';

View File

@ -0,0 +1,86 @@
import { Provide } from '@midwayjs/core';
import dayjs from 'dayjs';
import path from 'path';
import fs from 'fs';
import { cache, logger, utils } from '@certd/pipeline';
import { NotFoundException, ParamException, PermissionException } from '../../../basic/index.js';
export type UploadFileItem = {
filename: string;
tmpFilePath: string;
};
const uploadRootDir = './data/upload';
export const uploadTmpFileCacheKey = 'tmpfile_key_';
/**
*/
@Provide()
export class FileService {
async saveFile(userId: number, tmpCacheKey: any, permission: 'public' | 'private') {
if (tmpCacheKey.startsWith(`/${permission}`)) {
//已经保存过,不需要再次保存
return tmpCacheKey;
}
let fileName = '';
let tmpFilePath = tmpCacheKey;
if (uploadTmpFileCacheKey && tmpCacheKey.startsWith(uploadTmpFileCacheKey)) {
const tmpFile: UploadFileItem = cache.get(tmpCacheKey);
if (!tmpFile) {
throw new ParamException('文件已过期,请重新上传');
}
tmpFilePath = tmpFile.tmpFilePath;
fileName = tmpFile.filename || path.basename(tmpFilePath);
}
if (!tmpFilePath || !fs.existsSync(tmpFilePath)) {
throw new Error('文件不存在,请重新上传');
}
const date = dayjs().format('YYYY_MM_DD');
const random = Math.random().toString(36).substring(7);
const userIdMd5 = Buffer.from(Buffer.from(userId + '').toString('base64')).toString('hex');
const key = `/${permission}/${userIdMd5}/${date}/${random}_${fileName}`;
let savePath = path.join(uploadRootDir, key);
savePath = path.resolve(savePath);
const parentDir = path.dirname(savePath);
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true });
}
// eslint-disable-next-line node/no-unsupported-features/node-builtins
const copyFile = utils.promises.promisify(fs.copyFile);
await copyFile(tmpFilePath, savePath);
try {
fs.unlinkSync(tmpFilePath);
} catch (e) {
logger.error(e);
}
return key;
}
getFile(key: string, userId?: number) {
if (!key) {
throw new ParamException('参数错误');
}
if (key.indexOf('..') >= 0) {
//安全性判断
throw new ParamException('参数错误');
}
if (!key.startsWith('/')) {
throw new ParamException('参数错误');
}
const keyArr = key.split('/');
const permission = keyArr[1];
const userIdMd5 = keyArr[2];
if (permission !== 'public') {
//非公开文件需要验证用户
const userIdStr = Buffer.from(Buffer.from(userIdMd5, 'hex').toString('base64')).toString();
const userIdInt: number = parseInt(userIdStr, 10);
if (userId == null || userIdInt !== userId) {
throw new PermissionException('无访问权限');
}
}
const filePath = path.join(uploadRootDir, key);
if (!fs.existsSync(filePath)) {
throw new NotFoundException('文件不存在');
}
return filePath;
}
}

View File

@ -1,2 +1,2 @@
export * from './settings/index.js';
export * from './plus/index.js';
export * from './basic/index.js';

View File

@ -52,4 +52,5 @@ export class SysSiteInfo extends BaseSettings {
title?: string;
slogan?: string;
logo?: string;
loginLogo?: string;
}

View File

@ -6,6 +6,7 @@ VITE_APP_SLOGAN=让你的证书永不过期
VITE_APP_COPYRIGHT_YEAR=2021-2024
VITE_APP_COPYRIGHT_NAME=handsfree.work
VITE_APP_COPYRIGHT_URL=https://certd.handsfree.work
VITE_APP_LOGO=/statics/images/logo/logo.svg
VITE_APP_LOGO=/static/images/logo/logo.svg
VITE_APP_LOGIN_LOGO=/static/images/logo/rect-black.svg
VITE_APP_PROJECT_PATH=https://github.com/certd/certd

View File

@ -1,7 +0,0 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="500" height="500" viewBox="0 0 500.000000 500.000000"
>
<path d="M28.34 56.68h28.34V36.12H28.34a7.79 7.79 0 1 1 0-15.58h19.84v9.05h8.5V12H28.34a16.29 16.29 0 0 0 0 32.58h19.84v3.56H28.34a19.84 19.84 0 0 1 0-39.68h28.34V0H28.34a28.34 28.34 0 0 0 0 56.68z"
transform="translate(70, 76) scale(6,6)"
></path>
</svg>

Before

Width:  |  Height:  |  Size: 402 B

View File

@ -38,3 +38,10 @@ export async function bindUrl(data): Promise<SysInstallInfo> {
data
});
}
export async function getPlusInfo() {
return await request({
url: "/basic/settings/plusInfo",
method: "get"
});
}

View File

@ -64,10 +64,3 @@ export async function mine(): Promise<UserInfoRes> {
method: "post"
});
}
export async function getPlusInfo() {
return await request({
url: "/mine/plusInfo",
method: "post"
});
}

View File

@ -1,11 +1,12 @@
import { message, notification } from "ant-design-vue";
import { useUserStore } from "/@/store/modules/user";
import { notification } from "ant-design-vue";
import { useSettingStore } from "/@/store/modules/settings";
export default {
mounted(el: any, binding: any, vnode: any) {
const { value } = binding;
const userStore = useUserStore();
const settingStore = useSettingStore();
el.className = el.className + " need-plus";
if (!userStore.isPlus) {
if (!settingStore.isPlus) {
function checkPlus() {
// 事件处理代码
notification.warn({

View File

@ -12,14 +12,14 @@
</div>
</template>
<script lang="tsx" setup>
import { ref, reactive, computed } from "vue";
import { useUserStore } from "/src/store/modules/user";
import { computed, reactive } from "vue";
import dayjs from "dayjs";
import { message, Modal } from "ant-design-vue";
import * as api from "./api";
import { useSettingStore } from "/@/store/modules/settings";
import { useRouter } from "vue-router";
import { useUserStore } from "/@/store/modules/user";
const settingStore = useSettingStore();
const props = withDefaults(
defineProps<{
mode?: "button" | "nav" | "icon";
@ -33,7 +33,7 @@ type Text = {
title?: string;
};
const text = computed<Text>(() => {
const vipLabel = userStore.vipLabel;
const vipLabel = settingStore.vipLabel;
const map = {
isPlus: {
button: {
@ -64,26 +64,25 @@ const text = computed<Text>(() => {
}
}
};
if (userStore.isPlus) {
if (settingStore.isPlus) {
return map.isPlus[props.mode];
} else {
return map.free[props.mode];
}
});
const userStore = useUserStore();
const expireTime = computed(() => {
if (userStore.isPlus) {
return dayjs(userStore.plusInfo.expireTime).format("YYYY-MM-DD");
if (settingStore.isPlus) {
return dayjs(settingStore.plusInfo.expireTime).format("YYYY-MM-DD");
}
return "";
});
const expiredDays = computed(() => {
if (userStore.plusInfo?.isPlus && !userStore.isPlus) {
if (settingStore.plusInfo?.isPlus && !settingStore.isPlus) {
//
const days = dayjs().diff(dayjs(userStore.plusInfo.expireTime), "day");
return `${userStore.vipLabel}已过期${days}`;
const days = dayjs().diff(dayjs(settingStore.plusInfo.expireTime), "day");
return `${settingStore.vipLabel}已过期${days}`;
}
return "";
});
@ -92,6 +91,24 @@ const formState = reactive({
code: ""
});
const vipTypeDefine = {
free: {
title: "免费版",
type: "free",
privilege: ["证书申请功能无限制", "证书流水线数量10条", "常用的主机、cdn等部署插件"]
},
plus: {
title: "专业版",
type: "plus",
privilege: ["可加VIP群需求优先实现", "证书流水线数量无限制", "免配置发邮件功能", "支持宝塔、易盾、群晖、1Panel、cdnfly等部署插件"]
},
comm: {
title: "商业版",
type: "comm",
privilege: ["拥有专业版所有特权", "允许商用", "修改logo、标题", "多用户无限制", "支持用户支付(敬请期待)"]
}
};
const router = useRouter();
async function doActive() {
if (!formState.code) {
@ -101,10 +118,10 @@ async function doActive() {
const res = await api.doActive(formState);
if (res) {
await userStore.reInit();
const vipLabel = userStore.vipLabel;
const vipLabel = settingStore.vipLabel;
Modal.success({
title: "激活成功",
content: `您已成功激活${vipLabel},有效期至:${dayjs(userStore.plusInfo.expireTime).format("YYYY-MM-DD")}`,
content: `您已成功激活${vipLabel},有效期至:${dayjs(settingStore.plusInfo.expireTime).format("YYYY-MM-DD")}`,
onOk() {
if (!(settingStore.installInfo.bindUserId > 0)) {
//
@ -121,21 +138,20 @@ async function doActive() {
}
}
const settingStore = useSettingStore();
const computedSiteId = computed(() => settingStore.installInfo?.siteId);
const [modal, contextHolder] = Modal.useModal();
const userStore = useUserStore();
function openUpgrade() {
if (!userStore.isAdmin) {
message.info("仅限管理员操作");
return;
}
const placeholder = "请输入激活码";
const isPlus = userStore.isPlus;
const isPlus = settingStore.isPlus;
let title = "激活专业版/商业版";
if (userStore.isComm) {
if (settingStore.isComm) {
title = "续期商业版";
} else if (userStore.isPlus) {
} else if (settingStore.isPlus) {
title = "续期专业版/升级商业版";
}
@ -148,87 +164,35 @@ function openUpgrade() {
okText: "激活",
width: 900,
content: () => {
const vipLabel = userStore.vipLabel;
const vipLabel = settingStore.vipLabel;
const slots = [];
for (const key in vipTypeDefine) {
const item = vipTypeDefine[key];
const vipBlockClass = `vip-block ${key === settingStore.plusInfo.vipType ? "current" : ""}`;
slots.push(
<a-col span={8}>
<div class={vipBlockClass}>
<h3 class="block-header">{item.title}</h3>
<ul>
{item.privilege.map((p) => (
<li>
<fs-icon class="color-green" icon="ion:checkmark-sharp" />
{p}
</li>
))}
</ul>
</div>
</a-col>
);
}
return (
<div class="mt-10 mb-10 vip-active-modal">
<div class="vip-type-vs">
<a-row gutter={20}>
<a-col span={8}>
<h3 class="block-header">
免费版
<fs-icon v-if="!userStore.isPlus" class="color-green" icon="ion:checkmark-sharp" />
</h3>
<ul>
<li>
<fs-icon class="color-green" icon="ion:checkmark-sharp"></fs-icon>
</li>
<li>
<fs-icon class="color-green" icon="ion:checkmark-sharp"></fs-icon>
证书流水线数量10条
</li>
<li>
<fs-icon class="color-green" icon="ion:checkmark-sharp"></fs-icon>
常用的部署插件
</li>
</ul>
</a-col>
<a-col span={8}>
<h3 class="block-header">
专业版
<fs-icon v-if="userStore.isPlus && !userStore.isComm" class="color-green" icon="ion:checkmark-sharp" />
</h3>
<ul>
<li>
<fs-icon class="color-green" icon="ion:checkmark-sharp"></fs-icon>
可加VIP群需求优先实现
</li>
<li>
<fs-icon class="color-green" icon="ion:checkmark-sharp"></fs-icon>
证书流水线数量无限制
</li>
<li>
<fs-icon class="color-green" icon="ion:checkmark-sharp"></fs-icon>
免配置发邮件功能
</li>
<li>
<fs-icon class="color-green" icon="ion:checkmark-sharp"></fs-icon>
支持宝塔易盾群晖cdnfly1Panel等部署插件
</li>
</ul>
</a-col>
<a-col span={8}>
<h3 class="block-header">
商业版
<fs-icon v-if="userStore.isComm" class="color-green" icon="ion:checkmark-sharp" />
</h3>
<ul>
<li>
<fs-icon class="color-green" icon="ion:checkmark-sharp"></fs-icon>
拥有专业版所有特权
</li>
<li>
<fs-icon class="color-green" icon="ion:checkmark-sharp"></fs-icon>
修改logo标题
</li>
<li>
<fs-icon class="color-green" icon="ion:checkmark-sharp"></fs-icon>
多用户无限制
</li>
<li>
<fs-icon class="color-green" icon="ion:checkmark-sharp"></fs-icon>
支持用户支付
</li>
<li>
<fs-icon class="color-green" icon="ion:checkmark-sharp"></fs-icon>
允许商用
</li>
</ul>
</a-col>
</a-row>
<a-row gutter={20}>{slots}</a-row>
</div>
<div>
<div class="mt-10">
<h3 class="block-header">{isPlus ? "续期" : "立刻激活"}</h3>
<div>{isPlus ? `当前${vipLabel}已激活,到期时间` + dayjs(userStore.plusInfo.expireTime).format("YYYY-MM-DD") : ""}</div>
<div>{isPlus ? `当前${vipLabel}已激活,到期时间` + dayjs(settingStore.plusInfo.expireTime).format("YYYY-MM-DD") : ""}</div>
<div class="mt-10">
<div class="flex-o w-100">
<span>站点ID</span>
@ -269,6 +233,20 @@ function openUpgrade() {
}
.vip-active-modal {
.vip-block {
padding: 10px;
border: 1px solid #eee;
border-radius: 5px;
height: 160px;
//background-color: rgba(250, 237, 167, 0.79);
&.current {
border-color: green;
}
.block-header {
padding: 0px;
}
}
ul {
list-style-type: unset;
margin-left: 0px;

View File

@ -101,6 +101,11 @@ export default defineComponent({
return slots;
}
for (const sub of children) {
if (sub.meta?.show != null) {
if (sub.meta.show === false || (typeof sub.meta.show === "function" && !sub.meta.show())) {
continue;
}
}
const title: any = () => {
if (sub?.meta?.icon) {
// @ts-ignore

View File

@ -2,8 +2,8 @@
<a-layout class="fs-framework">
<a-layout-sider v-model:collapsed="asideCollapsed" :trigger="null" collapsible>
<div class="header-logo">
<img src="/static/images/logo/logo.svg" />
<span v-if="!asideCollapsed" class="title">Certd</span>
<img :src="siteInfo.logo" />
<span v-if="!asideCollapsed" class="title">{{ siteInfo.title }}</span>
</div>
<div class="aside-menu">
<fs-menu :scroll="true" :menus="asideMenus" :expand-selected="!asideCollapsed" />
@ -61,7 +61,18 @@
<a-layout-footer class="fs-framework-footer">
<div>
<span>Powered by</span>
<a href="https://certd.handsfree.work"> handsfree.work </a>
<a> handsfree.work </a>
<template v-if="siteInfo.icpNo">
<a-divider type="vertical" />
<span>
<a href="https://beian.miit.gov.cn/" target="_blank">{{ siteInfo.icpNo }}</a>
</span>
</template>
<template v-if="siteInfo.licenseTo">
<a-divider type="vertical" />
<a :href="siteInfo.licenseToUrl || ''">{{ siteInfo.licenseTo }}</a>
</template>
</div>
<div>v{{ version }}</div>
@ -71,11 +82,10 @@
</a-layout>
</template>
<script lang="ts">
<script lang="ts" setup>
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";
@ -83,69 +93,46 @@ import { usePageStore } from "/@/store/modules/page";
import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons-vue";
import FsThemeSet from "/@/layout/components/theme/index.vue";
import { env } from "../utils/util.env";
import FsThemeModeSet from "./components/theme/mode-set.vue";
import VipButton from "/@/components/vip-button/index.vue";
import TutorialButton from "/@/components/tutorial/index.vue";
import { useUserStore } from "/@/store/modules/user";
export default {
name: "LayoutFramework",
// eslint-disable-next-line vue/no-unused-components
components: {
TutorialButton,
FsThemeSet,
MenuFoldOutlined,
MenuUnfoldOutlined,
FsMenu,
FsLocale,
FsSourceLink,
FsUserInfo,
FsTabs,
FsThemeModeSet,
VipButton
},
setup() {
const resourceStore = useResourceStore();
const frameworkMenus = computed(() => {
import { useSettingStore } from "/@/store/modules/settings";
const resourceStore = useResourceStore();
const frameworkMenus = computed(() => {
return resourceStore.getFrameworkMenus;
});
const headerMenus = computed(() => {
});
const headerMenus = computed(() => {
return resourceStore.getHeaderMenus;
});
const asideMenus = computed(() => {
});
const asideMenus = computed(() => {
return resourceStore.getAsideMenus;
});
});
const pageStore = usePageStore();
const keepAlive = pageStore.keepAlive;
const pageStore = usePageStore();
const keepAlive = pageStore.keepAlive;
const asideCollapsed = ref(false);
function asideCollapsedToggle() {
const asideCollapsed = ref(false);
function asideCollapsedToggle() {
asideCollapsed.value = !asideCollapsed.value;
}
onErrorCaptured((e) => {
}
onErrorCaptured((e) => {
console.error("ErrorCaptured:", e);
// notification.error({ message: e.message });
//
return false;
});
const version = ref(import.meta.env.VITE_APP_VERSION);
});
const version = ref(import.meta.env.VITE_APP_VERSION);
const envRef = ref(env);
const envRef = ref(env);
const userStore = useUserStore();
return {
userStore,
version,
frameworkMenus,
headerMenus,
asideMenus,
keepAlive,
asideCollapsed,
asideCollapsedToggle,
envRef
};
}
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const siteInfo = computed(() => {
return settingStore.siteInfo;
});
</script>
<style lang="less">
@import "../style/theme/index.less";

View File

@ -1,13 +1,13 @@
<template>
<div id="userLayout" :class="['user-layout-wrapper']">
<div class="login-container flex-center">
<div class="user-layout-content">
<div class="user-layout-content flex-center flex-col">
<div class="top flex flex-col items-center justify-center">
<div class="header flex flex-row items-center">
<img src="/static/images/logo/rect-black.svg" class="logo" alt="logo" />
<img :src="logoRef" class="logo" alt="logo" />
<span class="title"></span>
</div>
<div class="desc"></div>
<div class="desc">{{ sloganRef }}</div>
</div>
<router-view />
@ -25,8 +25,13 @@
<span>
<a :href="envRef.COPYRIGHT_URL" target="_blank">{{ envRef.COPYRIGHT_NAME }}</a>
</span>
<span v-if="envRef.ICP_NO">
<a href="https://beian.miit.gov.cn/" target="_blank">{{ envRef.ICP_NO }}</a>
<span v-if="siteInfo.icpNo">
<a-divider type="vertical" />
<a href="https://beian.miit.gov.cn/" target="_blank">{{ siteInfo.icpNo }}</a>
</span>
<span v-if="siteInfo.licenseTo">
<a-divider type="vertical" />
<a :href="siteInfo.licenseToUrl" target="_blank">{{ siteInfo.licenseTo }}</a>
</span>
</div>
</div>
@ -34,19 +39,16 @@
</div>
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { env } from "/@/utils/util.env";
import { ref } from "vue";
import { computed, ref, Ref } from "vue";
import { SiteInfo, useSettingStore } from "/@/store/modules/settings";
export default {
name: "LayoutOutside",
setup() {
const envRef = ref(env);
return {
envRef
};
}
};
const envRef = ref(env);
const settingStore = useSettingStore();
const siteInfo: Ref<SiteInfo> = computed(() => {
return settingStore.siteInfo;
});
</script>
<style lang="less" scoped>
@ -70,24 +72,6 @@ export default {
//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;
@ -98,8 +82,8 @@ export default {
align-items: center;
justify-content: center;
.header {
height: 44px;
line-height: 44px;
height: 70px;
line-height: 70px;
.badge {
position: absolute;
@ -112,9 +96,8 @@ export default {
}
.logo {
height: 80px;
height: 100%;
vertical-align: top;
margin-right: 16px;
border-style: none;
}

View File

@ -152,4 +152,3 @@ const routes = [...outsideRoutes, ...frameworkRoutes];
const frameworkMenus = frameworkRet.menus;
const headerMenus = headerRet.menus;
export { routes, outsideRoutes, frameworkRoutes, frameworkMenus, headerMenus, findMenus, filterMenus };

View File

@ -1,4 +1,7 @@
import LayoutPass from "/@/layout/layout-pass.vue";
import { computed } from "vue";
import { useUserStore } from "/@/store/modules/user";
import { useSettingStore } from "/@/store/modules/settings";
export const sysResources = [
{
@ -56,16 +59,6 @@ export const sysResources = [
path: "/sys/authority/user",
component: "/sys/authority/user/index.vue"
},
{
title: "系统设置",
name: "settings",
meta: {
icon: "ion:settings-outline",
permission: "sys:settings:view"
},
path: "/sys/settings",
component: "/sys/settings/index.vue"
},
{
title: "账号绑定",
name: "account",
@ -76,10 +69,25 @@ export const sysResources = [
path: "/sys/account",
component: "/sys/account/index.vue"
},
{
title: "系统设置",
name: "settings",
meta: {
icon: "ion:settings-outline",
permission: "sys:settings:view"
},
path: "/sys/settings",
component: "/sys/settings/index.vue"
},
{
title: "站点个性化",
name: "site",
path: "/sys/site",
meta: {
show: () => {
const settingStore = useSettingStore();
return settingStore.isComm;
},
icon: "ion:document-text-outline",
permission: "sys:settings:view"
},
@ -87,18 +95,27 @@ export const sysResources = [
},
{
title: "商业版设置",
name: "/sys/commercial",
name: "SysCommercial",
meta: {
icon: "ion:document-text-outline",
permission: "sys:settings:view"
permission: "sys:settings:view",
show: () => {
const settingStore = useSettingStore();
return settingStore.isComm;
}
},
children: [
{
title: "套餐设置",
name: "suite",
path: "/sys/commercial/suite",
meta: {
icon: "ion:document-text-outline",
permission: "sys:settings:view"
permission: "sys:settings:view",
show: () => {
const settingStore = useSettingStore();
return settingStore.isComm;
}
}
}
]

View File

@ -1,5 +1,5 @@
import { defineStore } from "pinia";
import { Modal, theme } from "ant-design-vue";
import { Modal, notification, theme } from "ant-design-vue";
import _ from "lodash-es";
// @ts-ignore
import { LocalStorage } from "/src/utils/util.storage";
@ -8,6 +8,8 @@ import * as basicApi from "/@/api/modules/api.basic";
import { SysInstallInfo, SysPublicSetting } from "/@/api/modules/api.basic";
import { useUserStore } from "/@/store/modules/user";
import { mitter } from "/@/utils/util.mitt";
import { env } from "/@/utils/util.env";
import { toRef } from "vue";
export type ThemeToken = {
token: {
@ -31,11 +33,26 @@ export interface SettingState {
accountServerBaseUrl?: string;
appKey?: string;
};
siteInfo?: {
siteInfo: SiteInfo;
plusInfo?: PlusInfo;
}
export type SiteInfo = {
title: string;
slogan: string;
logo: string;
};
loginLogo: string;
warningOff: boolean;
icpNo: string;
licenseTo?: string;
licenseToUrl?: string;
};
interface PlusInfo {
vipType?: string;
expireTime?: number;
isPlus: boolean;
isComm?: boolean;
}
const defaultThemeConfig = {
@ -43,6 +60,16 @@ const defaultThemeConfig = {
mode: "light"
};
const SETTING_THEME_KEY = "SETTING_THEME";
const defaultSiteInfo = {
title: env.TITLE || "Certd",
slogan: env.SLOGAN || "让你的证书永不过期",
logo: env.LOGO || "/static/images/logo/logo.svg",
loginLogo: env.LOGIN_LOGO || "/static/images/logo/rect-block.svg",
warningOff: false,
icpNo: env.ICP_NO,
licenseTo: "",
licenseToUrl: ""
};
export const useSettingStore = defineStore({
id: "app.setting",
state: (): SettingState => ({
@ -51,6 +78,10 @@ export const useSettingStore = defineStore({
token: {},
algorithm: theme.defaultAlgorithm
},
plusInfo: {
isPlus: false,
vipType: "free"
},
sysPublic: {
registerEnabled: false,
managerOtherUserPipeline: false,
@ -63,11 +94,7 @@ export const useSettingStore = defineStore({
accountServerBaseUrl: "",
appKey: ""
},
siteInfo: {
title: "Certd",
slogan: "让你的证书永不过期",
logo: ""
}
siteInfo: defaultSiteInfo
}),
getters: {
getThemeConfig(): any {
@ -78,30 +105,75 @@ export const useSettingStore = defineStore({
},
getInstallInfo(): SysInstallInfo {
return this.installInfo;
},
isPlus(): boolean {
return this.plusInfo?.isPlus && this.plusInfo?.expireTime > new Date().getTime();
},
isComm(): boolean {
return this.plusInfo?.isComm && this.plusInfo?.expireTime > new Date().getTime();
},
vipLabel(): string {
const vipLabelMap: any = {
free: "免费版",
plus: "专业版",
comm: "商业版"
};
return vipLabelMap[this.plusInfo?.vipType || "free"];
}
},
actions: {
checkPlus() {
if (!this.isPlus) {
notification.warn({
message: "此为专业版功能,请先升级到专业版"
});
throw new Error("此为专业版功能,请升级到专业版");
}
},
async loadSysSettings() {
const settings = await basicApi.getSysPublicSettings();
_.merge(this.sysPublic, settings);
const userStore = useUserStore();
if (userStore.isComm) {
const siteInfo = await basicApi.getSiteInfo();
_.merge(this.siteInfo, siteInfo);
}
await this.loadInstallInfo();
await this.loadPlusInfo();
if (this.isComm) {
await this.loadSiteInfo();
}
await this.checkUrlBound();
},
async loadInstallInfo() {
const installInfo = await basicApi.getInstallInfo();
_.merge(this.installInfo, installInfo);
},
async loadPlusInfo() {
this.plusInfo = await basicApi.getPlusInfo();
},
async loadSiteInfo() {
const isComm = this.isComm;
let siteInfo = {};
if (isComm) {
siteInfo = await basicApi.getSiteInfo();
if (siteInfo.logo) {
siteInfo.logo = `/api/basic/file/download?key=${siteInfo.logo}`;
}
if (siteInfo.loginLogo) {
siteInfo.loginLogo = `/api/basic/file/download?key=${siteInfo.loginLogo}`;
}
}
const sysPublic = this.getSysPublic;
if (sysPublic.icpNo) {
siteInfo.icpNo = sysPublic.icpNo;
}
this.siteInfo = _.merge({}, defaultSiteInfo, siteInfo);
},
async checkUrlBound() {
const userStore = useUserStore();
if (!userStore.isAdmin || !userStore.isPlus) {
const settingStore = useSettingStore();
if (!userStore.isAdmin || !settingStore.isPlus) {
return;
}

View File

@ -15,17 +15,9 @@ import { mitter } from "/src/utils/util.mitt";
interface UserState {
userInfo: Nullable<UserInfoRes>;
token?: string;
plusInfo?: PlusInfo;
inited: boolean;
}
interface PlusInfo {
vipType: string;
expireTime: number;
isPlus: boolean;
isComm: boolean;
}
const USER_INFO_KEY = "USER_INFO";
const TOKEN_KEY = "TOKEN";
export const useUserStore = defineStore({
@ -35,8 +27,6 @@ export const useUserStore = defineStore({
userInfo: null,
// token
token: undefined,
// plus
plusInfo: null,
inited: false
}),
getters: {
@ -48,20 +38,6 @@ export const useUserStore = defineStore({
},
isAdmin(): boolean {
return this.getUserInfo.id === 1 || this.getUserInfo.roles?.includes(1);
},
isPlus(): boolean {
return this.plusInfo?.isPlus && this.plusInfo?.expireTime > new Date().getTime();
},
isComm(): boolean {
return this.plusInfo?.isComm && this.plusInfo?.expireTime > new Date().getTime();
},
vipLabel(): string {
const vipLabelMap: any = {
free: "免费版",
plus: "专业版",
comm: "商业版"
};
return vipLabelMap[this.plusInfo?.vipType || "free"];
}
},
actions: {
@ -79,14 +55,6 @@ export const useUserStore = defineStore({
LocalStorage.remove(TOKEN_KEY);
LocalStorage.remove(USER_INFO_KEY);
},
checkPlus() {
if (!this.isPlus) {
notification.warn({
message: "此为专业版功能,请先升级到专业版"
});
throw new Error("此为专业版功能,请升级到专业版");
}
},
async register(user: RegisterReq) {
await UserApi.register(user);
notification.success({
@ -118,16 +86,12 @@ export const useUserStore = defineStore({
async onLoginSuccess(loginData: any) {
await this.getUserInfoAction();
await this.loadPlusInfo();
const userInfo = await this.getUserInfoAction();
mitter.emit("app.login", { userInfo, token: loginData, plusInfo: this.plusInfo });
mitter.emit("app.login", { userInfo, token: loginData });
await router.replace("/");
return userInfo;
},
async loadPlusInfo() {
this.plusInfo = await UserApi.getPlusInfo();
},
/**
* @description: logout
*/
@ -155,9 +119,6 @@ export const useUserStore = defineStore({
if (this.inited) {
return;
}
if (this.getToken) {
await this.loadPlusInfo();
}
this.inited = true;
},
async reInit() {

View File

@ -199,3 +199,9 @@ h1, h2, h3, h4, h5, h6 {
.cursor-pointer{
cursor: pointer;
}
.helper{
display: inline-block;
color: #aeaeae;
font-size: 12px;
}

View File

@ -7,12 +7,13 @@ export class EnvConfig {
STORAGE: string = import.meta.env.VITE_APP_STORAGE;
TITLE: string = import.meta.env.VITE_APP_TITLE;
SLOGAN: string = import.meta.env.VITE_APP_SLOGAN;
LOGO: string = import.meta.env.VITE_APP_LOGO;
LOGIN_LOGO: string = import.meta.env.VITE_APP_LOGIN_LOGO;
ICP_NO: string = import.meta.env.VITE_APP_ICP_NO;
COPYRIGHT_YEAR: string = import.meta.env.VITE_APP_COPYRIGHT_YEAR;
COPYRIGHT_NAME: string = import.meta.env.VITE_APP_COPYRIGHT_NAME;
COPYRIGHT_URL: string = import.meta.env.VITE_APP_COPYRIGHT_URL;
LOGO: string = import.meta.env.VITE_APP_LOGO;
PM_ENABLED: string = import.meta.env.VITE_APP_PM_ENABLED;
ICP_NO: string = import.meta.env.VITE_APP_ICP_NO;
init(env: any) {
for (const key in this) {

View File

@ -102,7 +102,7 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
order: 101,
helper: {
render: () => {
if (userStore.isPlus) {
if (settingStore.isPlus) {
return "";
}
return (

View File

@ -211,7 +211,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
},
copy: {
click: async (context) => {
userStore.checkPlus();
settingStore.checkPlus();
const { ui } = useUi();
// @ts-ignore
let row = context[ui.tableColumn.row];

View File

@ -16,7 +16,7 @@
}"
/>
<a-alert v-if="!userStore.isPlus" class="m-1" type="info">
<a-alert v-if="!settingStore.isPlus" class="m-1" type="info">
<template #message> 还没有配置邮件服务器<router-link :to="{ path: '/certd/settings/email' }">现在就去</router-link> </template>
</a-alert>
</div>
@ -24,6 +24,7 @@
<script lang="ts" setup>
import { Ref, ref, watch } from "vue";
import { useUserStore } from "/@/store/modules/user";
import { useSettingStore } from "/@/store/modules/settings";
const props = defineProps({
options: {
@ -32,7 +33,7 @@ const props = defineProps({
}
});
const userStore = useUserStore();
const settingStore = useSettingStore();
const optionsFormState: Ref<any> = ref({});

View File

@ -114,6 +114,7 @@ import { PluginGroups } from "/@/views/certd/pipeline/pipeline/type";
import { useUserStore } from "/@/store/modules/user";
import { compute, useCompute } from "@fast-crud/fast-crud";
import { useReference } from "/@/use/use-refrence";
import { useSettingStore } from "/@/store/modules/settings";
export default {
name: "PiStepForm",
@ -132,7 +133,7 @@ export default {
* @returns
*/
function useStepForm() {
const useStore = useUserStore();
const settingStore = useSettingStore();
const getPluginGroups: any = inject("getPluginGroups");
const pluginGroups: PluginGroups = getPluginGroups();
const mode: Ref = ref("add");
@ -152,7 +153,7 @@ export default {
});
const stepTypeSelected = (item: any) => {
if (item.needPlus && !useStore.isPlus) {
if (item.needPlus && !settingStore.isPlus) {
message.warn("此插件需要开通专业版才能使用");
throw new Error("此插件需要开通专业版才能使用");
}

View File

@ -45,7 +45,7 @@
<a-button type="primary" @click="stepAdd(currentTask)"></a-button>
</template>
</a-descriptions>
<v-draggable v-model="currentTask.steps" class="step-list" handle=".handle" item-key="id" :disabled="!userStore.isPlus">
<v-draggable v-model="currentTask.steps" class="step-list" handle=".handle" item-key="id" :disabled="!settingStore.isPlus">
<template #item="{ element, index }">
<div class="step-row">
<div class="text">
@ -99,6 +99,7 @@ export default {
emits: ["update"],
setup(props: any, ctx: any) {
const userStore = useUserStore();
const settingStore = useSettingStore();
function useStep() {
const stepFormRef: Ref<any> = ref(null);
const currentStepIndex = ref(0);
@ -254,6 +255,7 @@ export default {
}
return {
userStore,
settingStore,
labelCol: { span: 6 },
wrapperCol: { span: 16 },
...useTaskForm(),

View File

@ -20,7 +20,7 @@
<div class="layout-left">
<div class="pipeline-container">
<div class="pipeline">
<v-draggable v-model="pipeline.stages" class="stages" item-key="id" handle=".stage-move-handle" :disabled="!userStore.isPlus">
<v-draggable v-model="pipeline.stages" class="stages" item-key="id" handle=".stage-move-handle" :disabled="!settingStore.isPlus">
<template #header>
<div class="stage first-stage">
<div class="title stage-move-handle">
@ -73,7 +73,7 @@
<fs-icon v-if="editMode" title="拖动排序" icon="ion:move-outline"></fs-icon>
</div>
</div>
<v-draggable v-model="stage.tasks" item-key="id" class="tasks" group="task" handle=".task-move-handle" :disabled="!userStore.isPlus">
<v-draggable v-model="stage.tasks" item-key="id" class="tasks" group="task" handle=".task-move-handle" :disabled="!settingStore.isPlus">
<template #item="{ element: task, index: taskIndex }">
<div
class="task-container"
@ -93,7 +93,7 @@
<a-popover title="步骤" :trigger="editMode ? 'none' : 'hover'">
<!-- :open="true"-->
<template #content>
<div v-for="(item, index) of task.steps" class="flex-o w-100">
<div v-for="(item, index) of task.steps" :key="item.id" class="flex-o w-100">
<span class="ellipsis flex-1 step-title" :class="{ disabled: item.disabled, deleted: item.disabled }">
{{ index + 1 }}. {{ item.title }}
</span>
@ -266,7 +266,6 @@ import { PipelineDetail, PipelineOptions, PluginGroups, RunHistory } from "./typ
import type { Runnable } from "@certd/pipeline";
import PiHistoryTimelineItem from "/@/views/certd/pipeline/pipeline/component/history-timeline-item.vue";
import { FsIcon } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/modules/user";
export default defineComponent({
name: "PipelineEdit",
// eslint-disable-next-line vue/no-unused-components
@ -301,8 +300,6 @@ export default defineComponent({
router.back();
}
const userStore = useUserStore();
const loadCurrentHistoryDetail = async () => {
console.log("load history logs");
const detail: RunHistory = await props.options?.getHistoryDetail({ historyId: currentHistory.value.id });
@ -678,13 +675,13 @@ export default defineComponent({
const useTaskRet = useTask();
const useStageRet = useStage(useTaskRet);
const settingStore = useSettingStore();
return {
pipeline,
currentHistory,
histories,
goBack,
userStore,
settingStore,
...useTaskRet,
...useStageRet,
...useTrigger(),

View File

@ -54,7 +54,7 @@
<a-form :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }">
<a-form-item label="使用官方邮件服务器">
<div class="flex-o">
<a-switch v-model:checked="formState.usePlus" :disabled="!userStore.isPlus" @change="onUsePlusChanged" />
<a-switch v-model:checked="formState.usePlus" :disabled="!settingStore.isPlus" @change="onUsePlusChanged" />
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<div class="helper">使用官方邮箱服务器直接发邮件免除繁琐的配置</div>
@ -83,7 +83,7 @@ import * as emailApi from "./api.email";
import { SettingKeys } from "./api";
import { notification } from "ant-design-vue";
import { useUserStore } from "/@/store/modules/user";
import { useSettingStore } from "/@/store/modules/settings";
interface FormState {
host: string;
@ -154,7 +154,7 @@ async function onTestSend() {
}
}
const userStore = useUserStore();
const settingStore = useSettingStore();
</script>
<style lang="less">

View File

@ -2,11 +2,11 @@
<div class="d2-page-cover">
<div class="d2-page-cover__title" @click="$open('https://github.com/certd/certd')">
<div class="title-line">
<span>Certd v{{ version }}</span>
<span>{{ siteInfo.title }} v{{ version }}</span>
</div>
</div>
<p class="d2-page-cover__sub-title">让你的证书永不过期</p>
<div class="warnning">
<p class="d2-page-cover__sub-title">{{ siteInfo.slogan }}</p>
<div v-if="siteInfo.warningOff !== false" class="warning">
<a-alert type="warning" show-icon>
<template #description>
<div class="flex">
@ -27,17 +27,14 @@
</div>
</div>
</template>
<script>
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "PageContent",
setup() {
const version = ref(import.meta.env.VITE_APP_VERSION);
<script lang="ts" setup>
import { computed, ref, Ref } from "vue";
import { SiteInfo, useSettingStore } from "/@/store/modules/settings";
return {
version
};
}
const version = ref(import.meta.env.VITE_APP_VERSION);
const settingStore = useSettingStore();
const siteInfo: Ref<SiteInfo> = computed(() => {
return settingStore.siteInfo;
});
</script>
<style lang="less" scoped>

View File

@ -1,31 +0,0 @@
export default {
crud: ` columns: {
date:{
title: '姓名', //字段名称
type: 'text', //字段类型,添加、修改、查询将自动生成相应表单组件
},
province: {
title: '城市',
type: 'dict-select', //选择框
form: { //表单组件自定义配置,此处配置选择框为多选
component: { //支持任何v-model组件
filterable: true, multiple: true, clearable: true
}
},
dict: dict({
data: [ //本地数据字典
{ value: 'sz', label: '深圳' },
{ value: 'gz', label: '广州' },
{ value: 'wh', label: '武汉' },
{ value: 'sh', label: '上海' }
]
})
},
status: {
title: '状态',
type: 'dict-select', //选择框,默认单选
dict: dict({ url: '/dicts/OpenStatusEnum' })//远程数据字典
},
}
`
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -1,178 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
height="210mm"
viewBox="0 0 210 210"
version="1.1"
id="svg8"
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)"
sodipodi:docname="logo.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.45420139"
inkscape:cx="628.76623"
inkscape:cy="688.02672"
inkscape:document-units="mm"
inkscape:current-layer="svg8"
inkscape:document-rotation="0"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:snap-bbox="true"
inkscape:snap-nodes="false"
inkscape:snap-bbox-edge-midpoints="false"
inkscape:bbox-nodes="true"
inkscape:bbox-paths="false"
inkscape:snap-global="true"
showguides="true"
inkscape:guide-bbox="true">
<sodipodi:guide
position="-128.15534,201.26213"
orientation="1,0"
id="guide1012" />
<sodipodi:guide
position="333.7864,165.14563"
orientation="1,0"
id="guide1014" />
<sodipodi:guide
position="242.91262,73.689318"
orientation="1,0"
id="guide1016" />
<sodipodi:guide
position="105.83717,102.82499"
orientation="1,0"
id="guide1022" />
<sodipodi:guide
position="138.0814,102.82499"
orientation="0,-1"
id="guide1024" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="图层 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline">
<path
style="fill:#002255;stroke:none;stroke-width:0.625348"
d="M 35.587501,69.766667 V 59.766664 h 70.000109 69.99991 v 10.000003 9.999997 H 105.58761 35.587501 Z"
id="path12" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-2"
width="32.244232"
height="20"
x="71.506088"
y="106.64581" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-8-8"
width="32.244232"
height="20"
x="107.42467"
y="106.64581" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-8-5-8"
width="32.244232"
height="20"
x="143.34325"
y="106.64581" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-2-4"
width="32.244232"
height="20"
x="71.506088"
y="129.82079" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-8-8-3"
width="32.244232"
height="20"
x="107.42467"
y="129.82079" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-8-5-8-2"
width="32.244232"
height="20"
x="143.34325"
y="129.82079" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-2-7"
width="32.244232"
height="20"
x="35.587502"
y="106.64581" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-2-4-0"
width="32.244232"
height="20"
x="35.587502"
y="129.82079" />
<rect
style="display:inline;fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-2-9"
width="32.244232"
height="20"
x="71.506088"
y="82.941666" />
<rect
style="display:inline;fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-8-8-37"
width="32.244232"
height="20"
x="107.42467"
y="82.941666" />
<rect
style="display:inline;fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-8-5-8-4"
width="32.244232"
height="20"
x="143.34325"
y="82.941666" />
<rect
style="display:inline;fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-2-7-1"
width="32.244232"
height="20"
x="35.587502"
y="82.941666" />
</g>
<polygon
points="75.3,174.4 103.1,103.6 79.8,103.6 112.6,41.3 156.4,41.3 129.9,90.5 148.1,90.5 "
fill="#f6cc00"
id="polygon276"
transform="matrix(1.0930933,0,0,0.99853202,-17.517362,-0.52287941)" />
</svg>

Before

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -1,136 +0,0 @@
<template>
<div class="d2-page-cover">
<div class="d2-page-cover__title">
<div class="title-line">
<img width="50" :src="envRef.LOGO" />
{{ envRef.TITLE }} v{{ version }}
</div>
</div>
<p class="d2-page-cover__sub-title">{{ envRef.SLOGAN }}</p>
<div class="exampleBox">
<div class="left">
<fs-highlight :code="helperRef.crud" lang="javascript" />
</div>
<div class="icon">
<fs-icon :icon="ui.icons.arrowRight" />
</div>
<div class="right">
<img style="border: 1px solid #eee" src="./image/crud.png" />
</div>
</div>
<div class="footer_box">
<div class="left"></div>
<div class="right">
<div>如果觉得好用请不要吝啬你的star哟</div>
<a href="https://gitee.com/fast-crud/fast-crud" target="_blank"><img src="https://gitee.com/fast-crud/fast-crud/badge/star.svg?theme=dark" alt="star" /></a>
<a href="https://github.com/fast-crud/fast-crud" target="_blank"><img alt="GitHub stars" src="https://img.shields.io/github/stars/fast-crud/fast-crud?logo=github" /></a>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { useUi } from "@fast-crud/fast-crud";
import helper from "./helper";
import { env } from "../../../../utils/util.env";
export default defineComponent({
name: "PageCover",
setup() {
const version = ref(import.meta.env.VITE_APP_VERSION);
const helperRef = ref(helper);
const { ui } = useUi();
const envRef = ref(env);
return {
ui,
version,
helperRef,
envRef
};
}
});
</script>
<style lang="less" scoped>
.d2-page-cover {
.logo {
width: 40px;
height: 40px;
}
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
.d2-page-cover__logo {
img {
width: 200px;
}
}
.d2-page-cover__title {
margin: 20px;
font-weight: bold;
display: -webkit-flex; /* Safari */
display: flex;
justify-content: flex-end;
.title-line {
display: flex;
align-items: center;
flex-direction: row;
justify-content: center;
cursor: pointer;
font-size: 20px;
}
}
.d2-page-cover__sub-title {
margin: 0px;
margin-bottom: 10px;
}
.d2-page-cover__build-time {
margin: 0px;
margin-bottom: 0px;
margin-top: 10px;
font-size: 12px;
}
.exampleBox {
display: flex;
align-items: center;
height: 390px;
width: 96%;
padding: 5px;
margin: auto;
justify-content: center;
.left {
height: 100%;
overflow-y: hidden;
border: 1px solid #aaa;
}
.icon {
padding: 10px;
font-size: 20px;
}
.right {
height: 100%;
img {
height: 100%;
}
}
.d2-highlight {
font-size: 8px;
}
}
.footer_box {
padding: 10px;
display: flex;
justify-content: center;
align-items: center;
.right {
display: flex;
justify-items: center;
align-items: center;
& > * {
display: flex;
}
}
}
}
</style>

View File

@ -64,13 +64,9 @@
</a-input>
</a-col>
<a-col class="gutter-row" :span="8">
<a-button
class="getCaptcha"
tabindex="-1"
:disabled="smsSendBtnDisabled"
@click="sendSmsCode"
v-text="smsTime <= 0 ? '发送' : smsTime + ' s'"
></a-button>
<a-button class="getCaptcha" tabindex="-1" :disabled="smsSendBtnDisabled" @click="sendSmsCode">
{{ smsTime <= 0 ? "发送" : smsTime + " s" }}
</a-button>
</a-col>
</a-row>
</a-form-item>

View File

@ -47,8 +47,8 @@ onMounted(() => {
const subjectInfo: SubjectInfo = {
subjectId: settingStore.installInfo.siteId,
installTime: settingStore.installInfo.installTime,
vipType: userStore.plusInfo.vipType || "free",
expiresTime: userStore.plusInfo.expireTime
vipType: settingStore.plusInfo.vipType || "free",
expiresTime: settingStore.plusInfo.expireTime
};
return subjectInfo;
});
@ -74,7 +74,7 @@ onMounted(() => {
await userStore.reInit();
notification.success({
message: "更新成功",
description: "专业版已激活"
description: "专业版/商业版已激活"
});
});
});

View File

@ -20,14 +20,14 @@
<a-switch v-model:checked="formState.managerOtherUserPipeline" />
</a-form-item>
<a-form-item label="ICP备案号" name="icpNo">
<a-switch v-model:checked="formState.icpNo" />
<a-input v-model:value="formState.icpNo" />
</a-form-item>
<!-- <a-form-item label="启动后触发流水线" name="triggerOnStartup">-->
<!-- <a-switch v-model:checked="formState.triggerOnStartup" />-->
<!-- <div class="helper">启动后自动触发一次所有已启用的流水线</div>-->
<!-- </a-form-item>-->
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">保存</a-button>
<a-button :loading="saveLoading" type="primary" html-type="submit">保存</a-button>
</a-form-item>
</a-form>
@ -41,7 +41,7 @@
</template>
<script setup lang="ts">
import { reactive } from "vue";
import { reactive, ref } from "vue";
import * as api from "./api";
import { PublicSettingsSave, SettingKeys } from "./api";
import { notification } from "ant-design-vue";
@ -59,18 +59,27 @@ const formState = reactive<Partial<FormState>>({
async function loadSysPublicSettings() {
const data: any = await api.SettingsGet(SettingKeys.SysPublic);
if (data == null) {
return;
}
const setting = JSON.parse(data.setting);
Object.assign(formState, setting);
}
const saveLoading = ref(false);
loadSysPublicSettings();
const settingsStore = useSettingStore();
const onFinish = async (form: any) => {
try {
saveLoading.value = true;
await api.PublicSettingsSave(form);
await settingsStore.loadSysSettings();
notification.success({
message: "保存成功"
});
} finally {
saveLoading.value = false;
}
};
const onFinishFailed = (errorInfo: any) => {

View File

@ -13,8 +13,6 @@ export async function SettingsSave(setting: any) {
await request({
url: apiPrefix + "/save",
method: "post",
data: {
setting: JSON.stringify(setting)
}
data: setting
});
}

View File

@ -13,17 +13,42 @@
@finish="onFinish"
@finish-failed="onFinishFailed"
>
<a-form-item label="标题" name="title">
<a-input v-model:checked="formState.title" />
<a-form-item label="站点名称" name="title">
<a-input v-model:value="formState.title" />
</a-form-item>
<a-form-item label="副标题" name="slogan">
<a-form-item label="副标题/口号" name="slogan">
<a-input v-model:value="formState.slogan" />
</a-form-item>
<a-form-item label="Logo" name="logo">
<fs-cropper-upload v-model:value="formState.logo" />
<fs-cropper-uploader
v-model:model-value="formState.logo"
:cropper="cropperOptions"
value-type="key"
:uploader="uploaderConfig"
:build-url="buildUrl"
/>
</a-form-item>
<a-form-item label="登录页Logo" name="loginLogo">
<fs-cropper-uploader
v-model:model-value="formState.loginLogo"
:cropper="loginLogoCropperOptions"
value-type="key"
:uploader="uploaderConfig"
:build-url="buildUrl"
/>
</a-form-item>
<a-form-item label="关闭首页告警" name="warningOff">
<a-switch v-model:checked="formState.warningOff" />
</a-form-item>
<a-form-item label="你的主体名称" name="licenseTo">
<a-input v-model:value="formState.licenseTo" />
<div class="helper">将会显示在底部</div>
</a-form-item>
<a-form-item label="你的主体URL" name="licenseToUrl">
<a-input v-model:value="formState.licenseToUrl" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">保存</a-button>
<a-button :loading="saveLoading" type="primary" html-type="submit">保存</a-button>
</a-form-item>
</a-form>
@ -37,16 +62,20 @@
</template>
<script setup lang="ts">
import { reactive } from "vue";
import { reactive, ref } from "vue";
import * as api from "./api";
import { notification } from "ant-design-vue";
import { useSettingStore } from "/src/store/modules/settings";
import { useUserStore } from "/@/store/modules/user";
interface FormState {
title: string;
slogan: string;
logo: string;
icpNo: string;
loginLogo: string;
warningOff: boolean;
licenseTo: string;
licenseToUrl: string;
}
const formState = reactive<Partial<FormState>>({});
@ -56,19 +85,56 @@ async function loadSysSiteSettings() {
if (data == null) {
return;
}
const setting = JSON.parse(data.setting);
Object.assign(formState, setting);
Object.assign(formState, data);
}
const saveLoading = ref(false);
loadSysSiteSettings();
const settingsStore = useSettingStore();
const onFinish = async (form: any) => {
saveLoading.value = true;
try {
await api.SettingsSave(form);
await loadSysSiteSettings();
await settingsStore.loadSysSettings();
notification.success({
message: "保存成功"
});
} finally {
saveLoading.value = false;
}
};
const userStore = useUserStore();
const uploaderConfig = ref({
type: "form",
action: "/basic/file/upload",
name: "file",
headers: {
Authorization: "Bearer " + userStore.getToken
},
successHandle(res: any) {
return res;
}
});
function buildUrl(key: string) {
return `/api/basic/file/download?&key=` + key;
}
function onFinishFailed(err) {
console.log(err);
}
const cropperOptions = ref({
aspectRatio: 1,
autoCropArea: 1,
viewMode: 0
});
const loginLogoCropperOptions = ref({
aspectRatio: 3,
autoCropArea: 1,
viewMode: 0
});
</script>
<style lang="less">
@ -78,4 +144,8 @@ const onFinish = async (form: any) => {
margin: 20px;
}
}
.fs-cropper-dialog__preview img {
border-radius: 0 !important;
margin-top: 0 !important;
}
</style>

View File

@ -1,21 +0,0 @@
<template>
<fs-page>
<template #header>
<div class="title">input输入框</div>
</template>
<component :is="ui.card.name">
<fs-ui-render :render-fn="inputDemo1Render"></fs-ui-render>
</component>
</fs-page>
</template>
<script lang="ts" setup>
import { useUi } from "@fast-crud/fast-crud";
import { ref } from "vue";
const { ui } = useUi();
const textRef = ref();
function inputDemo1Render() {
return ui.input.render({ vModel: { ref: textRef, key: "value" } });
}
</script>

View File

@ -0,0 +1,39 @@
# key: ./data/ssl/cert.key
# cert: ./data/ssl/cert.crt
#plus:
# server:
# baseUrl: 'http://127.0.0.1:11007'
#flyway:
# scriptDir: './db/migration-pg'
#typeorm:
# dataSource:
# default:
# type: postgres
# host: localhost
# port: 5433
# username: postgres
# password: root
# database: postgres
typeorm:
dataSource:
default:
database: './data/db-comm.sqlite'
#plus:
# server:
# baseUrls: ['https://api.ai.handsfree.work', 'https://api.ai.docmirror.cn']
#
#account:
# server:
# baseUrl: 'https://ai.handsfree.work/subject'
plus:
server:
baseUrls: ['http://127.0.0.1:11007']
account:
server:
baseUrl: 'http://127.0.0.1:1017/subject'

View File

@ -7,6 +7,7 @@
"scripts": {
"start": "cross-env NODE_ENV=production node ./bootstrap.js",
"dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app",
"commdev": "cross-env NODE_ENV=commdev mwtsc --watch --run @midwayjs/mock/app",
"pgdev": "cross-env NODE_ENV=pgdev mwtsc --watch --run @midwayjs/mock/app",
"test": "cross-env NODE_ENV=unittest mocha",
"cov": "cross-env c8 --all --reporter=text --reporter=lcovonly npm run test",
@ -23,11 +24,11 @@
"@alicloud/cs20151215": "^3.0.3",
"@alicloud/pop-core": "^1.7.10",
"@certd/acme-client": "^1.25.9",
"@certd/lib-huawei": "^1.25.9",
"@certd/lib-server": "^1.25.9",
"@certd/commercial-core": "^1.25.9",
"@certd/lib-huawei": "^1.25.9",
"@certd/lib-jdcloud": "^1.25.9",
"@certd/lib-k8s": "^1.25.9",
"@certd/lib-server": "^1.25.9",
"@certd/midway-flyway-js": "^1.25.9",
"@certd/pipeline": "^1.25.9",
"@certd/plugin-cert": "^1.25.9",
@ -43,6 +44,7 @@
"@midwayjs/logger": "^3.1.0",
"@midwayjs/static-file": "^3.16.4",
"@midwayjs/typeorm": "^3.16.4",
"@midwayjs/upload": "3",
"@midwayjs/validate": "^3.16.4",
"ali-oss": "^6.21.0",
"axios": "^1.7.2",

View File

@ -14,7 +14,9 @@ import { PipelineEntity } from '../modules/pipeline/entity/pipeline.js';
import { mergeConfig } from './loader.js';
import { libServerEntities } from '@certd/lib-server';
import { commercialEntities } from '@certd/commercial-core';
import { tmpdir } from 'node:os';
import { uploadWhiteList, DefaultUploadFileMimeType } from '@midwayjs/upload';
import path from 'path';
const env = process.env.NODE_ENV || 'development';
const development = {
@ -90,6 +92,23 @@ const development = {
plus: {
serverBaseUrls: ['http://127.0.0.1:11007'],
},
upload: {
// mode: UploadMode, 默认为file即上传到服务器临时目录可以配置为 stream
mode: 'file',
// fileSize: string, 最大上传文件大小,默认为 10mb
fileSize: '10mb',
whitelist: uploadWhiteList, //文件扩展名白名单
mimeTypeWhiteList: DefaultUploadFileMimeType, //文件MIME类型白名单
// whitelist: uploadWhiteList.filter(ext => ext !== '.pdf'),
// tmpdir: string上传的文件临时存储路径
tmpdir: path.join(tmpdir(), 'certd-upload-files'),
// cleanTimeout: number上传的文件在临时目录中多久之后自动删除默认为 5 分钟
cleanTimeout: 5 * 60 * 1000,
// base64: boolean设置原始body是否是base64格式默认为false一般用于腾讯云的兼容
base64: false,
// 仅在匹配路径到 /api/upload 的时候去解析 body 中的文件信息
match: /\/api\/basic\/file\/upload/,
},
} as MidwayConfig;
mergeConfig(development, 'development');

View File

@ -16,6 +16,7 @@ import { ResetPasswdMiddleware } from './middleware/reset-passwd/middleware.js';
import DefaultConfig from './config/config.default.js';
import * as libServer from '@certd/lib-server';
import * as commercial from '@certd/commercial-core';
import * as upload from '@midwayjs/upload';
process.on('uncaughtException', error => {
console.error('未捕获的异常:', error);
// 在这里可以添加日志记录、发送错误通知等操作
@ -30,12 +31,13 @@ process.on('uncaughtException', error => {
cron,
staticFile,
validate,
upload,
libServer,
commercial,
{
component: info,
enabledEnvironment: ['local'],
},
libServer,
commercial,
],
importConfigs: [
{

View File

@ -50,11 +50,11 @@ export class AuthorityMiddleware implements IWebMiddleware {
let token = ctx.get('Authorization') || '';
token = token.replace('Bearer ', '').trim();
if (token === '') {
if (!token) {
//尝试从cookie中获取token
token = ctx.cookies.get('token') || '';
}
if (token === '') {
if (!token) {
//尝试从query中获取token
token = (ctx.query.token as string) || '';
}

View File

@ -0,0 +1,46 @@
import { Controller, Fields, Files, Get, Inject, Post, Provide, Query } from '@midwayjs/core';
import { BaseController, Constants, FileService, UploadFileItem, uploadTmpFileCacheKey } from '@certd/lib-server';
import send from 'koa-send';
import { nanoid } from 'nanoid';
import { cache } from '@certd/pipeline';
import { UploadFileInfo } from '@midwayjs/upload';
/**
*/
@Provide()
@Controller('/api/basic/file')
export class FileController extends BaseController {
@Inject()
fileService: FileService;
@Post('/upload', { summary: 'sys:settings:view' })
async upload(@Files() files: UploadFileInfo<string>[], @Fields() fields: any) {
console.log('files', files, fields);
const cacheKey = uploadTmpFileCacheKey + nanoid();
const file = files[0];
cache.set(
cacheKey,
{
filename: file.filename,
tmpFilePath: file.data,
} as UploadFileItem,
{
ttl: 1000 * 60 * 60,
}
);
return this.ok({
key: cacheKey,
});
}
@Get('/download', { summary: Constants.per.guest })
async download(@Query('key') key: string) {
let userId: any = null;
if (!key.startsWith('/public')) {
userId = this.getUserId();
}
const filePath = this.fileService.getFile(key, userId);
this.ctx.response.attachment(filePath);
await send(this.ctx, filePath);
}
}

View File

@ -1,9 +1,6 @@
import { Config, Controller, Get, Inject, Provide } from '@midwayjs/core';
import { BaseController } from '@certd/lib-server';
import { Constants } from '@certd/lib-server';
import { SysSettingsService } from '@certd/lib-server';
import { SysInstallInfo, SysPublicSettings, SysSiteInfo } from '@certd/lib-server';
import { AppKey } from '@certd/pipeline';
import { ALL, Body, Config, Controller, Get, Inject, Provide } from '@midwayjs/core';
import { BaseController, Constants, SysInstallInfo, SysPublicSettings, SysSettingsService, SysSiteInfo } from '@certd/lib-server';
import { AppKey, getPlusInfo } from '@certd/pipeline';
/**
*/
@ -34,4 +31,12 @@ export class BasicSettingsController extends BaseController {
const settings: SysSiteInfo = await this.sysSettingsService.getSetting(SysSiteInfo);
return this.ok(settings);
}
@Get('/plusInfo', { summary: Constants.per.guest })
async plusInfo(@Body(ALL) body: any) {
const info = getPlusInfo();
return this.ok({
...info,
});
}
}

View File

@ -1,8 +1,6 @@
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { BaseController } from '@certd/lib-server';
import { Constants } from '@certd/lib-server';
import { BaseController, Constants } from '@certd/lib-server';
import { UserService } from '../../authority/service/user-service.js';
import { getPlusInfo } from '@certd/pipeline';
import { RoleService } from '../../authority/service/role-service.js';
/**
@ -29,12 +27,4 @@ export class MineController extends BaseController {
await this.userService.changePassword(userId, body);
return this.ok({});
}
@Post('/plusInfo', { summary: Constants.per.authOnly })
async plusInfo(@Body(ALL) body) {
const info = getPlusInfo();
return this.ok({
...info,
});
}
}