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", "node-forge": "^1.3.1",
"nodemailer": "^6.9.3", "nodemailer": "^6.9.3",
"proxy-agent": "^6.4.0", "proxy-agent": "^6.4.0",
"qs": "^6.11.2" "qs": "^6.11.2",
"dayjs": "^1.11.7"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^23.0.4", "@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 { fileUtils } from "./util.file.js";
import _ from "lodash-es"; import _ from "lodash-es";
import { cache } from "./util.cache.js"; import { cache } from "./util.cache.js";
import dayjs from 'dayjs';
export const utils = { export const utils = {
sleep, sleep,
http, http,
@ -27,4 +29,5 @@ export const utils = {
mergeUtils, mergeUtils,
cache, cache,
nanoid, 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 = { export const promises = {
TimeoutPromise, TimeoutPromise,
safePromise, safePromise,
promisify,
}; };

View File

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

View File

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

View File

@ -1,7 +1,9 @@
export * from './auth-exception.js' export * from './auth-exception.js';
export * from './base-exception.js' export * from './base-exception.js';
export * from './permission-exception.js' export * from './permission-exception.js';
export * from './preview-exception.js' export * from './preview-exception.js';
export * from './validation-exception.js' export * from './validation-exception.js';
export * from './vip-exception.js' export * from './vip-exception.js';
export * from './common-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 { export class PermissionException extends BaseException {
constructor(message?: string) { constructor(message?: string) {
super( super('PermissionException', Constants.res.permission.code, message ? message : Constants.res.permission.message);
'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 { export class ValidateException extends BaseException {
constructor(message) { constructor(message) {
super( super('ValidateException', Constants.res.validation.code, message ? message : Constants.res.validation.message);
'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/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 './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; title?: string;
slogan?: string; slogan?: string;
logo?: string; logo?: string;
loginLogo?: string;
} }

View File

@ -6,6 +6,7 @@ VITE_APP_SLOGAN=让你的证书永不过期
VITE_APP_COPYRIGHT_YEAR=2021-2024 VITE_APP_COPYRIGHT_YEAR=2021-2024
VITE_APP_COPYRIGHT_NAME=handsfree.work VITE_APP_COPYRIGHT_NAME=handsfree.work
VITE_APP_COPYRIGHT_URL=https://certd.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 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 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" 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 { notification } from "ant-design-vue";
import { useUserStore } from "/@/store/modules/user"; import { useSettingStore } from "/@/store/modules/settings";
export default { export default {
mounted(el: any, binding: any, vnode: any) { mounted(el: any, binding: any, vnode: any) {
const { value } = binding; const { value } = binding;
const userStore = useUserStore(); const settingStore = useSettingStore();
el.className = el.className + " need-plus"; el.className = el.className + " need-plus";
if (!userStore.isPlus) { if (!settingStore.isPlus) {
function checkPlus() { function checkPlus() {
// 事件处理代码 // 事件处理代码
notification.warn({ notification.warn({

View File

@ -12,14 +12,14 @@
</div> </div>
</template> </template>
<script lang="tsx" setup> <script lang="tsx" setup>
import { ref, reactive, computed } from "vue"; import { computed, reactive } from "vue";
import { useUserStore } from "/src/store/modules/user";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { message, Modal } from "ant-design-vue"; import { message, Modal } from "ant-design-vue";
import * as api from "./api"; import * as api from "./api";
import { useSettingStore } from "/@/store/modules/settings"; import { useSettingStore } from "/@/store/modules/settings";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useUserStore } from "/@/store/modules/user";
const settingStore = useSettingStore();
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
mode?: "button" | "nav" | "icon"; mode?: "button" | "nav" | "icon";
@ -33,7 +33,7 @@ type Text = {
title?: string; title?: string;
}; };
const text = computed<Text>(() => { const text = computed<Text>(() => {
const vipLabel = userStore.vipLabel; const vipLabel = settingStore.vipLabel;
const map = { const map = {
isPlus: { isPlus: {
button: { button: {
@ -64,26 +64,25 @@ const text = computed<Text>(() => {
} }
} }
}; };
if (userStore.isPlus) { if (settingStore.isPlus) {
return map.isPlus[props.mode]; return map.isPlus[props.mode];
} else { } else {
return map.free[props.mode]; return map.free[props.mode];
} }
}); });
const userStore = useUserStore();
const expireTime = computed(() => { const expireTime = computed(() => {
if (userStore.isPlus) { if (settingStore.isPlus) {
return dayjs(userStore.plusInfo.expireTime).format("YYYY-MM-DD"); return dayjs(settingStore.plusInfo.expireTime).format("YYYY-MM-DD");
} }
return ""; return "";
}); });
const expiredDays = computed(() => { const expiredDays = computed(() => {
if (userStore.plusInfo?.isPlus && !userStore.isPlus) { if (settingStore.plusInfo?.isPlus && !settingStore.isPlus) {
// //
const days = dayjs().diff(dayjs(userStore.plusInfo.expireTime), "day"); const days = dayjs().diff(dayjs(settingStore.plusInfo.expireTime), "day");
return `${userStore.vipLabel}已过期${days}`; return `${settingStore.vipLabel}已过期${days}`;
} }
return ""; return "";
}); });
@ -92,6 +91,24 @@ const formState = reactive({
code: "" 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(); const router = useRouter();
async function doActive() { async function doActive() {
if (!formState.code) { if (!formState.code) {
@ -101,10 +118,10 @@ async function doActive() {
const res = await api.doActive(formState); const res = await api.doActive(formState);
if (res) { if (res) {
await userStore.reInit(); await userStore.reInit();
const vipLabel = userStore.vipLabel; const vipLabel = settingStore.vipLabel;
Modal.success({ Modal.success({
title: "激活成功", title: "激活成功",
content: `您已成功激活${vipLabel},有效期至:${dayjs(userStore.plusInfo.expireTime).format("YYYY-MM-DD")}`, content: `您已成功激活${vipLabel},有效期至:${dayjs(settingStore.plusInfo.expireTime).format("YYYY-MM-DD")}`,
onOk() { onOk() {
if (!(settingStore.installInfo.bindUserId > 0)) { if (!(settingStore.installInfo.bindUserId > 0)) {
// //
@ -121,21 +138,20 @@ async function doActive() {
} }
} }
const settingStore = useSettingStore();
const computedSiteId = computed(() => settingStore.installInfo?.siteId); const computedSiteId = computed(() => settingStore.installInfo?.siteId);
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const userStore = useUserStore();
function openUpgrade() { function openUpgrade() {
if (!userStore.isAdmin) { if (!userStore.isAdmin) {
message.info("仅限管理员操作"); message.info("仅限管理员操作");
return; return;
} }
const placeholder = "请输入激活码"; const placeholder = "请输入激活码";
const isPlus = userStore.isPlus; const isPlus = settingStore.isPlus;
let title = "激活专业版/商业版"; let title = "激活专业版/商业版";
if (userStore.isComm) { if (settingStore.isComm) {
title = "续期商业版"; title = "续期商业版";
} else if (userStore.isPlus) { } else if (settingStore.isPlus) {
title = "续期专业版/升级商业版"; title = "续期专业版/升级商业版";
} }
@ -148,87 +164,35 @@ function openUpgrade() {
okText: "激活", okText: "激活",
width: 900, width: 900,
content: () => { 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 ( return (
<div class="mt-10 mb-10 vip-active-modal"> <div class="mt-10 mb-10 vip-active-modal">
<div class="vip-type-vs"> <div class="vip-type-vs">
<a-row gutter={20}> <a-row gutter={20}>{slots}</a-row>
<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>
</div> </div>
<div> <div class="mt-10">
<h3 class="block-header">{isPlus ? "续期" : "立刻激活"}</h3> <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="mt-10">
<div class="flex-o w-100"> <div class="flex-o w-100">
<span>站点ID</span> <span>站点ID</span>
@ -269,6 +233,20 @@ function openUpgrade() {
} }
.vip-active-modal { .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 { ul {
list-style-type: unset; list-style-type: unset;
margin-left: 0px; margin-left: 0px;

View File

@ -101,6 +101,11 @@ export default defineComponent({
return slots; return slots;
} }
for (const sub of children) { 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 = () => { const title: any = () => {
if (sub?.meta?.icon) { if (sub?.meta?.icon) {
// @ts-ignore // @ts-ignore

View File

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

View File

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

View File

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

View File

@ -1,4 +1,7 @@
import LayoutPass from "/@/layout/layout-pass.vue"; 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 = [ export const sysResources = [
{ {
@ -56,16 +59,6 @@ export const sysResources = [
path: "/sys/authority/user", path: "/sys/authority/user",
component: "/sys/authority/user/index.vue" 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: "账号绑定", title: "账号绑定",
name: "account", name: "account",
@ -76,10 +69,25 @@ export const sysResources = [
path: "/sys/account", path: "/sys/account",
component: "/sys/account/index.vue" 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: "站点个性化", title: "站点个性化",
name: "site", name: "site",
path: "/sys/site",
meta: { meta: {
show: () => {
const settingStore = useSettingStore();
return settingStore.isComm;
},
icon: "ion:document-text-outline", icon: "ion:document-text-outline",
permission: "sys:settings:view" permission: "sys:settings:view"
}, },
@ -87,18 +95,27 @@ export const sysResources = [
}, },
{ {
title: "商业版设置", title: "商业版设置",
name: "/sys/commercial", name: "SysCommercial",
meta: { meta: {
icon: "ion:document-text-outline", icon: "ion:document-text-outline",
permission: "sys:settings:view" permission: "sys:settings:view",
show: () => {
const settingStore = useSettingStore();
return settingStore.isComm;
}
}, },
children: [ children: [
{ {
title: "套餐设置", title: "套餐设置",
name: "suite", name: "suite",
path: "/sys/commercial/suite",
meta: { meta: {
icon: "ion:document-text-outline", 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 { defineStore } from "pinia";
import { Modal, theme } from "ant-design-vue"; import { Modal, notification, theme } from "ant-design-vue";
import _ from "lodash-es"; import _ from "lodash-es";
// @ts-ignore // @ts-ignore
import { LocalStorage } from "/src/utils/util.storage"; 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 { SysInstallInfo, SysPublicSetting } from "/@/api/modules/api.basic";
import { useUserStore } from "/@/store/modules/user"; import { useUserStore } from "/@/store/modules/user";
import { mitter } from "/@/utils/util.mitt"; import { mitter } from "/@/utils/util.mitt";
import { env } from "/@/utils/util.env";
import { toRef } from "vue";
export type ThemeToken = { export type ThemeToken = {
token: { token: {
@ -31,11 +33,26 @@ export interface SettingState {
accountServerBaseUrl?: string; accountServerBaseUrl?: string;
appKey?: string; appKey?: string;
}; };
siteInfo?: { siteInfo: SiteInfo;
plusInfo?: PlusInfo;
}
export type SiteInfo = {
title: string; title: string;
slogan: string; slogan: string;
logo: 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 = { const defaultThemeConfig = {
@ -43,6 +60,16 @@ const defaultThemeConfig = {
mode: "light" mode: "light"
}; };
const SETTING_THEME_KEY = "SETTING_THEME"; 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({ export const useSettingStore = defineStore({
id: "app.setting", id: "app.setting",
state: (): SettingState => ({ state: (): SettingState => ({
@ -51,6 +78,10 @@ export const useSettingStore = defineStore({
token: {}, token: {},
algorithm: theme.defaultAlgorithm algorithm: theme.defaultAlgorithm
}, },
plusInfo: {
isPlus: false,
vipType: "free"
},
sysPublic: { sysPublic: {
registerEnabled: false, registerEnabled: false,
managerOtherUserPipeline: false, managerOtherUserPipeline: false,
@ -63,11 +94,7 @@ export const useSettingStore = defineStore({
accountServerBaseUrl: "", accountServerBaseUrl: "",
appKey: "" appKey: ""
}, },
siteInfo: { siteInfo: defaultSiteInfo
title: "Certd",
slogan: "让你的证书永不过期",
logo: ""
}
}), }),
getters: { getters: {
getThemeConfig(): any { getThemeConfig(): any {
@ -78,30 +105,75 @@ export const useSettingStore = defineStore({
}, },
getInstallInfo(): SysInstallInfo { getInstallInfo(): SysInstallInfo {
return this.installInfo; 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: { actions: {
checkPlus() {
if (!this.isPlus) {
notification.warn({
message: "此为专业版功能,请先升级到专业版"
});
throw new Error("此为专业版功能,请升级到专业版");
}
},
async loadSysSettings() { async loadSysSettings() {
const settings = await basicApi.getSysPublicSettings(); const settings = await basicApi.getSysPublicSettings();
_.merge(this.sysPublic, settings); _.merge(this.sysPublic, settings);
const userStore = useUserStore();
if (userStore.isComm) {
const siteInfo = await basicApi.getSiteInfo();
_.merge(this.siteInfo, siteInfo);
}
await this.loadInstallInfo(); await this.loadInstallInfo();
await this.loadPlusInfo();
if (this.isComm) {
await this.loadSiteInfo();
}
await this.checkUrlBound(); await this.checkUrlBound();
}, },
async loadInstallInfo() { async loadInstallInfo() {
const installInfo = await basicApi.getInstallInfo(); const installInfo = await basicApi.getInstallInfo();
_.merge(this.installInfo, installInfo); _.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() { async checkUrlBound() {
const userStore = useUserStore(); const userStore = useUserStore();
if (!userStore.isAdmin || !userStore.isPlus) { const settingStore = useSettingStore();
if (!userStore.isAdmin || !settingStore.isPlus) {
return; return;
} }

View File

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

View File

@ -199,3 +199,9 @@ h1, h2, h3, h4, h5, h6 {
.cursor-pointer{ .cursor-pointer{
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; STORAGE: string = import.meta.env.VITE_APP_STORAGE;
TITLE: string = import.meta.env.VITE_APP_TITLE; TITLE: string = import.meta.env.VITE_APP_TITLE;
SLOGAN: string = import.meta.env.VITE_APP_SLOGAN; 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_YEAR: string = import.meta.env.VITE_APP_COPYRIGHT_YEAR;
COPYRIGHT_NAME: string = import.meta.env.VITE_APP_COPYRIGHT_NAME; COPYRIGHT_NAME: string = import.meta.env.VITE_APP_COPYRIGHT_NAME;
COPYRIGHT_URL: string = import.meta.env.VITE_APP_COPYRIGHT_URL; 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; PM_ENABLED: string = import.meta.env.VITE_APP_PM_ENABLED;
ICP_NO: string = import.meta.env.VITE_APP_ICP_NO;
init(env: any) { init(env: any) {
for (const key in this) { for (const key in this) {

View File

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

View File

@ -211,7 +211,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
}, },
copy: { copy: {
click: async (context) => { click: async (context) => {
userStore.checkPlus(); settingStore.checkPlus();
const { ui } = useUi(); const { ui } = useUi();
// @ts-ignore // @ts-ignore
let row = context[ui.tableColumn.row]; 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> <template #message> 还没有配置邮件服务器<router-link :to="{ path: '/certd/settings/email' }">现在就去</router-link> </template>
</a-alert> </a-alert>
</div> </div>
@ -24,6 +24,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Ref, ref, watch } from "vue"; import { Ref, ref, watch } from "vue";
import { useUserStore } from "/@/store/modules/user"; import { useUserStore } from "/@/store/modules/user";
import { useSettingStore } from "/@/store/modules/settings";
const props = defineProps({ const props = defineProps({
options: { options: {
@ -32,7 +33,7 @@ const props = defineProps({
} }
}); });
const userStore = useUserStore(); const settingStore = useSettingStore();
const optionsFormState: Ref<any> = ref({}); 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 { useUserStore } from "/@/store/modules/user";
import { compute, useCompute } from "@fast-crud/fast-crud"; import { compute, useCompute } from "@fast-crud/fast-crud";
import { useReference } from "/@/use/use-refrence"; import { useReference } from "/@/use/use-refrence";
import { useSettingStore } from "/@/store/modules/settings";
export default { export default {
name: "PiStepForm", name: "PiStepForm",
@ -132,7 +133,7 @@ export default {
* @returns * @returns
*/ */
function useStepForm() { function useStepForm() {
const useStore = useUserStore(); const settingStore = useSettingStore();
const getPluginGroups: any = inject("getPluginGroups"); const getPluginGroups: any = inject("getPluginGroups");
const pluginGroups: PluginGroups = getPluginGroups(); const pluginGroups: PluginGroups = getPluginGroups();
const mode: Ref = ref("add"); const mode: Ref = ref("add");
@ -152,7 +153,7 @@ export default {
}); });
const stepTypeSelected = (item: any) => { const stepTypeSelected = (item: any) => {
if (item.needPlus && !useStore.isPlus) { if (item.needPlus && !settingStore.isPlus) {
message.warn("此插件需要开通专业版才能使用"); message.warn("此插件需要开通专业版才能使用");
throw new Error("此插件需要开通专业版才能使用"); throw new Error("此插件需要开通专业版才能使用");
} }

View File

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

View File

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

View File

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

View File

@ -2,11 +2,11 @@
<div class="d2-page-cover"> <div class="d2-page-cover">
<div class="d2-page-cover__title" @click="$open('https://github.com/certd/certd')"> <div class="d2-page-cover__title" @click="$open('https://github.com/certd/certd')">
<div class="title-line"> <div class="title-line">
<span>Certd v{{ version }}</span> <span>{{ siteInfo.title }} v{{ version }}</span>
</div> </div>
</div> </div>
<p class="d2-page-cover__sub-title">让你的证书永不过期</p> <p class="d2-page-cover__sub-title">{{ siteInfo.slogan }}</p>
<div class="warnning"> <div v-if="siteInfo.warningOff !== false" class="warning">
<a-alert type="warning" show-icon> <a-alert type="warning" show-icon>
<template #description> <template #description>
<div class="flex"> <div class="flex">
@ -27,17 +27,14 @@
</div> </div>
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import { defineComponent, ref } from "vue"; import { computed, ref, Ref } from "vue";
export default defineComponent({ import { SiteInfo, useSettingStore } from "/@/store/modules/settings";
name: "PageContent",
setup() {
const version = ref(import.meta.env.VITE_APP_VERSION);
return { const version = ref(import.meta.env.VITE_APP_VERSION);
version const settingStore = useSettingStore();
}; const siteInfo: Ref<SiteInfo> = computed(() => {
} return settingStore.siteInfo;
}); });
</script> </script>
<style lang="less" scoped> <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-input>
</a-col> </a-col>
<a-col class="gutter-row" :span="8"> <a-col class="gutter-row" :span="8">
<a-button <a-button class="getCaptcha" tabindex="-1" :disabled="smsSendBtnDisabled" @click="sendSmsCode">
class="getCaptcha" {{ smsTime <= 0 ? "发送" : smsTime + " s" }}
tabindex="-1" </a-button>
:disabled="smsSendBtnDisabled"
@click="sendSmsCode"
v-text="smsTime <= 0 ? '发送' : smsTime + ' s'"
></a-button>
</a-col> </a-col>
</a-row> </a-row>
</a-form-item> </a-form-item>

View File

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

View File

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

View File

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

View File

@ -13,17 +13,42 @@
@finish="onFinish" @finish="onFinish"
@finish-failed="onFinishFailed" @finish-failed="onFinishFailed"
> >
<a-form-item label="标题" name="title"> <a-form-item label="站点名称" name="title">
<a-input v-model:checked="formState.title" /> <a-input v-model:value="formState.title" />
</a-form-item> </a-form-item>
<a-form-item label="副标题" name="slogan"> <a-form-item label="副标题/口号" name="slogan">
<a-input v-model:value="formState.slogan" /> <a-input v-model:value="formState.slogan" />
</a-form-item> </a-form-item>
<a-form-item label="Logo" name="logo"> <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>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }"> <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-item>
</a-form> </a-form>
@ -37,16 +62,20 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive } from "vue"; import { reactive, ref } from "vue";
import * as api from "./api"; import * as api from "./api";
import { notification } from "ant-design-vue"; import { notification } from "ant-design-vue";
import { useSettingStore } from "/src/store/modules/settings"; import { useSettingStore } from "/src/store/modules/settings";
import { useUserStore } from "/@/store/modules/user";
interface FormState { interface FormState {
title: string; title: string;
slogan: string; slogan: string;
logo: string; logo: string;
icpNo: string; loginLogo: string;
warningOff: boolean;
licenseTo: string;
licenseToUrl: string;
} }
const formState = reactive<Partial<FormState>>({}); const formState = reactive<Partial<FormState>>({});
@ -56,19 +85,56 @@ async function loadSysSiteSettings() {
if (data == null) { if (data == null) {
return; return;
} }
const setting = JSON.parse(data.setting); Object.assign(formState, data);
Object.assign(formState, setting);
} }
const saveLoading = ref(false);
loadSysSiteSettings(); loadSysSiteSettings();
const settingsStore = useSettingStore(); const settingsStore = useSettingStore();
const onFinish = async (form: any) => { const onFinish = async (form: any) => {
saveLoading.value = true;
try {
await api.SettingsSave(form); await api.SettingsSave(form);
await loadSysSiteSettings();
await settingsStore.loadSysSettings(); await settingsStore.loadSysSettings();
notification.success({ notification.success({
message: "保存成功" 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> </script>
<style lang="less"> <style lang="less">
@ -78,4 +144,8 @@ const onFinish = async (form: any) => {
margin: 20px; margin: 20px;
} }
} }
.fs-cropper-dialog__preview img {
border-radius: 0 !important;
margin-top: 0 !important;
}
</style> </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": { "scripts": {
"start": "cross-env NODE_ENV=production node ./bootstrap.js", "start": "cross-env NODE_ENV=production node ./bootstrap.js",
"dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app", "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", "pgdev": "cross-env NODE_ENV=pgdev mwtsc --watch --run @midwayjs/mock/app",
"test": "cross-env NODE_ENV=unittest mocha", "test": "cross-env NODE_ENV=unittest mocha",
"cov": "cross-env c8 --all --reporter=text --reporter=lcovonly npm run test", "cov": "cross-env c8 --all --reporter=text --reporter=lcovonly npm run test",
@ -23,11 +24,11 @@
"@alicloud/cs20151215": "^3.0.3", "@alicloud/cs20151215": "^3.0.3",
"@alicloud/pop-core": "^1.7.10", "@alicloud/pop-core": "^1.7.10",
"@certd/acme-client": "^1.25.9", "@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/commercial-core": "^1.25.9",
"@certd/lib-huawei": "^1.25.9",
"@certd/lib-jdcloud": "^1.25.9", "@certd/lib-jdcloud": "^1.25.9",
"@certd/lib-k8s": "^1.25.9", "@certd/lib-k8s": "^1.25.9",
"@certd/lib-server": "^1.25.9",
"@certd/midway-flyway-js": "^1.25.9", "@certd/midway-flyway-js": "^1.25.9",
"@certd/pipeline": "^1.25.9", "@certd/pipeline": "^1.25.9",
"@certd/plugin-cert": "^1.25.9", "@certd/plugin-cert": "^1.25.9",
@ -43,6 +44,7 @@
"@midwayjs/logger": "^3.1.0", "@midwayjs/logger": "^3.1.0",
"@midwayjs/static-file": "^3.16.4", "@midwayjs/static-file": "^3.16.4",
"@midwayjs/typeorm": "^3.16.4", "@midwayjs/typeorm": "^3.16.4",
"@midwayjs/upload": "3",
"@midwayjs/validate": "^3.16.4", "@midwayjs/validate": "^3.16.4",
"ali-oss": "^6.21.0", "ali-oss": "^6.21.0",
"axios": "^1.7.2", "axios": "^1.7.2",

View File

@ -14,7 +14,9 @@ import { PipelineEntity } from '../modules/pipeline/entity/pipeline.js';
import { mergeConfig } from './loader.js'; import { mergeConfig } from './loader.js';
import { libServerEntities } from '@certd/lib-server'; import { libServerEntities } from '@certd/lib-server';
import { commercialEntities } from '@certd/commercial-core'; 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 env = process.env.NODE_ENV || 'development';
const development = { const development = {
@ -90,6 +92,23 @@ const development = {
plus: { plus: {
serverBaseUrls: ['http://127.0.0.1:11007'], 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; } as MidwayConfig;
mergeConfig(development, 'development'); 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 DefaultConfig from './config/config.default.js';
import * as libServer from '@certd/lib-server'; import * as libServer from '@certd/lib-server';
import * as commercial from '@certd/commercial-core'; import * as commercial from '@certd/commercial-core';
import * as upload from '@midwayjs/upload';
process.on('uncaughtException', error => { process.on('uncaughtException', error => {
console.error('未捕获的异常:', error); console.error('未捕获的异常:', error);
// 在这里可以添加日志记录、发送错误通知等操作 // 在这里可以添加日志记录、发送错误通知等操作
@ -30,12 +31,13 @@ process.on('uncaughtException', error => {
cron, cron,
staticFile, staticFile,
validate, validate,
upload,
libServer,
commercial,
{ {
component: info, component: info,
enabledEnvironment: ['local'], enabledEnvironment: ['local'],
}, },
libServer,
commercial,
], ],
importConfigs: [ importConfigs: [
{ {

View File

@ -50,11 +50,11 @@ export class AuthorityMiddleware implements IWebMiddleware {
let token = ctx.get('Authorization') || ''; let token = ctx.get('Authorization') || '';
token = token.replace('Bearer ', '').trim(); token = token.replace('Bearer ', '').trim();
if (token === '') { if (!token) {
//尝试从cookie中获取token //尝试从cookie中获取token
token = ctx.cookies.get('token') || ''; token = ctx.cookies.get('token') || '';
} }
if (token === '') { if (!token) {
//尝试从query中获取token //尝试从query中获取token
token = (ctx.query.token as string) || ''; 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 { ALL, Body, Config, Controller, Get, Inject, Provide } from '@midwayjs/core';
import { BaseController } from '@certd/lib-server'; import { BaseController, Constants, SysInstallInfo, SysPublicSettings, SysSettingsService, SysSiteInfo } from '@certd/lib-server';
import { Constants } from '@certd/lib-server'; import { AppKey, getPlusInfo } from '@certd/pipeline';
import { SysSettingsService } from '@certd/lib-server';
import { SysInstallInfo, SysPublicSettings, SysSiteInfo } from '@certd/lib-server';
import { AppKey } from '@certd/pipeline';
/** /**
*/ */
@ -34,4 +31,12 @@ export class BasicSettingsController extends BaseController {
const settings: SysSiteInfo = await this.sysSettingsService.getSetting(SysSiteInfo); const settings: SysSiteInfo = await this.sysSettingsService.getSetting(SysSiteInfo);
return this.ok(settings); 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 { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { BaseController } from '@certd/lib-server'; import { BaseController, Constants } from '@certd/lib-server';
import { Constants } from '@certd/lib-server';
import { UserService } from '../../authority/service/user-service.js'; import { UserService } from '../../authority/service/user-service.js';
import { getPlusInfo } from '@certd/pipeline';
import { RoleService } from '../../authority/service/role-service.js'; import { RoleService } from '../../authority/service/role-service.js';
/** /**
@ -29,12 +27,4 @@ export class MineController extends BaseController {
await this.userService.changePassword(userId, body); await this.userService.changePassword(userId, body);
return this.ok({}); return this.ok({});
} }
@Post('/plusInfo', { summary: Constants.per.authOnly })
async plusInfo(@Body(ALL) body) {
const info = getPlusInfo();
return this.ok({
...info,
});
}
} }