mirror of https://github.com/certd/certd
feat: 站点个性化设置
parent
ce9a9862f1
commit
11a9fe9014
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -44,6 +44,14 @@ export const Constants = {
|
|||
code: 402,
|
||||
message: '您没有权限',
|
||||
},
|
||||
param: {
|
||||
code: 400,
|
||||
message: '参数错误',
|
||||
},
|
||||
notFound: {
|
||||
code: 404,
|
||||
message: '页面/文件/资源不存在',
|
||||
},
|
||||
preview: {
|
||||
code: 10001,
|
||||
message: '对不起,预览环境不允许修改此数据',
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export * from './service/plus-service.js';
|
||||
export * from './service/file-service.js';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,2 +1,2 @@
|
|||
export * from './settings/index.js';
|
||||
export * from './plus/index.js';
|
||||
export * from './basic/index.js';
|
||||
|
|
|
@ -52,4 +52,5 @@ export class SysSiteInfo extends BaseSettings {
|
|||
title?: string;
|
||||
slogan?: string;
|
||||
logo?: string;
|
||||
loginLogo?: string;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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"
|
||||
});
|
||||
}
|
||||
|
|
|
@ -64,10 +64,3 @@ export async function mine(): Promise<UserInfoRes> {
|
|||
method: "post"
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPlusInfo() {
|
||||
return await request({
|
||||
url: "/mine/plusInfo",
|
||||
method: "post"
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>
|
||||
支持宝塔、易盾、群晖、cdnfly、1Panel等部署插件
|
||||
</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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -152,4 +152,3 @@ const routes = [...outsideRoutes, ...frameworkRoutes];
|
|||
const frameworkMenus = frameworkRet.menus;
|
||||
const headerMenus = headerRet.menus;
|
||||
export { routes, outsideRoutes, frameworkRoutes, frameworkMenus, headerMenus, findMenus, filterMenus };
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -199,3 +199,9 @@ h1, h2, h3, h4, h5, h6 {
|
|||
.cursor-pointer{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.helper{
|
||||
display: inline-block;
|
||||
color: #aeaeae;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -102,7 +102,7 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
|
|||
order: 101,
|
||||
helper: {
|
||||
render: () => {
|
||||
if (userStore.isPlus) {
|
||||
if (settingStore.isPlus) {
|
||||
return "";
|
||||
}
|
||||
return (
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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({});
|
||||
|
||||
|
|
|
@ -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("此插件需要开通专业版才能使用");
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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: "专业版/商业版已激活"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -13,8 +13,6 @@ export async function SettingsSave(setting: any) {
|
|||
await request({
|
||||
url: apiPrefix + "/save",
|
||||
method: "post",
|
||||
data: {
|
||||
setting: JSON.stringify(setting)
|
||||
}
|
||||
data: setting
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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'
|
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
@ -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) || '';
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue