More translation

This commit is contained in:
Lorenzo
2025-06-27 01:31:31 +02:00
parent adc3ab7e0a
commit fc1084ce33
18 changed files with 2772 additions and 2253 deletions
@@ -447,4 +447,237 @@ export default {
subdomainManagement: "Subdomain Management", subdomainManagement: "Subdomain Management",
isDisabled: "Is Disabled", isDisabled: "Is Disabled",
enabled: "Enabled", enabled: "Enabled",
uploadCustomCert: "Upload Custom Certificate",
sourcee: "Source",
sourcePipeline: "Pipeline",
sourceManualUpload: "Manual Upload",
domains: "Domains",
enterDomain: "Please enter domain",
validDays: "Valid Days",
expires: " expires",
days: " days",
expireTime: "Expiration Time",
certIssuer: "Certificate Issuer",
applyTime: "Application Time",
relatedPipeline: "Related Pipeline",
statusSuccess: "Success",
statusChecking: "Checking",
statusError: "Error",
actionImportBatch: "Batch Import",
actionSyncIp: "Sync IP",
modalTitleSyncIp: "Sync IP",
modalContentSyncIp: "Are you sure to sync IP?",
notificationSyncComplete: "Sync Complete",
actionCheckAll: "Check All",
modalTitleConfirm: "Confirm",
modalContentCheckAll: "Confirm to trigger checking all IP site's certificates?",
notificationCheckSubmitted: "Check task submitted",
notificationCheckDescription: "Please refresh later to see results",
tooltipCheckNow: "Check Now",
notificationCheckSubmittedPleaseRefresh: "Check task submitted, please refresh later",
columnId: "ID",
columnIp: "IP",
helperIpCname: "Supports entering CNAME domain name",
ruleIpRequired: "Please enter IP",
columnCertDomains: "Certificate Domains",
columnCertProvider: "Issuer",
columnCertStatus: "Certificate Status",
statusNormal: "Normal",
statusExpired: "Expired",
columnCertExpiresTime: "Certificate Expiration Time",
expired: "expired",
columnCheckStatus: "Check Status",
columnLastCheckTime: "Last Check Time",
columnSource: "Source",
sourceSync: "Sync",
sourceManual: "Manual",
sourceImport: "Import",
columnDisabled: "Enabled/Disabled",
columnRemark: "Remark",
pluginFile: "Plugin File",
selectPluginFile: "Select plugin file",
overrideSameName: "Override same name",
override: "Override",
noOverride: "No override",
overrideHelper: "If a plugin with the same name exists, override it directly",
importPlugin: "Import Plugin",
operationSuccess: "Operation successful",
customPlugin: "Custom Plugin",
import: "Import",
export: "Export",
pluginType: "Plugin Type",
auth: "Authorization",
dns: "DNS",
deployPlugin: "Deploy Plugin",
icon: "Icon",
pluginName: "Plugin Name",
pluginNameHelper: "Must be English letters or digits, camelCase with type prefix\nExample: AliyunDeployToCDN\nDo not modify name once plugin is used",
pluginNameRuleMsg: "Must be English letters or digits, camelCase with type prefix",
author: "Author",
authorHelper: "Used as prefix when uploading to plugin store, e.g., greper/pluginName",
authorRuleMsg: "Must be English letters or digits",
titleHelper: "Plugin name in Chinese",
descriptionHelper: "Description of the plugin",
builtIn: "Built-in",
custom: "Custom",
store: "Store",
version: "Version",
pluginDependencies: "Plugin Dependencies",
pluginDependenciesHelper: "Dependencies to install first in format: [author/]pluginName[:version]",
editableRunStrategy: "Editable Run Strategy",
editable: "Editable",
notEditable: "Not Editable",
runStrategy: "Run Strategy",
normalRun: "Normal Run",
skipOnSuccess: "Skip on success (Deploy task)",
defaultRunStrategyHelper: "Default run strategy",
enableDisable: "Enable/Disable",
clickToToggle: "Click to toggle enable/disable",
confirmToggle: "Are you sure to",
disable: "disable",
enable: "enable",
pluginGroup: "Plugin Group",
icpRegistrationNumber: "ICP Registration Number",
icpPlaceholder: "Guangdong ICP xxxxxxx Number",
publicSecurityRegistrationNumber: "Public Security Registration Number",
publicSecurityPlaceholder: "Beijing Public Security xxxxxxx Number",
enableAssistant: "Enable Assistant",
allowCrawlers: "Allow Crawlers",
httpProxy: "HTTP Proxy",
httpProxyPlaceholder: "http://192.168.1.2:18010/",
httpProxyHelper: "Configure when some websites are blocked",
httpsProxy: "HTTPS Proxy",
httpsProxyPlaceholder: "http://192.168.1.2:18010/",
saveThenTestTitle: "Save first, then click test",
testButton: "Test",
httpsProxyHelper: "Usually both proxies are the same, save first then test",
dualStackNetwork: "Dual Stack Network",
default: "Default",
ipv4Priority: "IPv4 Priority",
ipv6Priority: "IPv6 Priority",
dualStackNetworkHelper: "If IPv6 priority is selected, enable IPv6 in docker-compose.yaml",
enableCommonCnameService: "Enable Public CNAME Service",
commonCnameHelper: "Allow use of public CNAME service. If disabled and no <router-link to='/sys/cname/provider'>custom CNAME service</router-link> is set, CNAME proxy certificate application will not work.",
saveButton: "Save",
stopSuccess: "Stopped successfully",
google: "Google",
baidu: "Baidu",
success: "Success",
testFailed: "Test Failed",
testCompleted: "Test Completed",
manageOtherUserPipeline: "Manage other users' pipelines",
limitUserPipelineCount: "Limit user pipeline count",
limitUserPipelineCountHelper: "0 means no limit",
enableSelfRegistration: "Enable self-registration",
enableUserValidityPeriod: "Enable user validity period",
userValidityPeriodHelper: "Users can use normally within validity; pipelines disabled after expiry",
enableUsernameRegistration: "Enable username registration",
enableEmailRegistration: "Enable email registration",
proFeature: "Pro feature",
emailServerSetup: "Set up email server",
enableSmsLoginRegister: "Enable SMS login and registration",
commFeature: "Commercial feature",
smsProvider: "SMS provider",
aliyunSms: "Aliyun SMS",
yfySms: "YFY SMS",
smsTest: "SMS test",
testMobilePlaceholder: "Enter test mobile number",
saveThenTest: "Save first then test",
enterTestMobile: "Please enter test mobile number",
sendSuccess: "Sent successfully",
atLeastOneLoginRequired: "At least one of password login or SMS login must be enabled",
fieldRequired: "This field is required",
siteHide: "Site Hide",
enableSiteHide: "Enable Site Hide",
siteHideDescription: "You can disable site accessibility normally and enable it when needed to enhance site security",
helpDoc: "Help Document",
randomAddress: "Random Address",
siteHideUrlHelper: "After the site is hidden, you need to visit this URL to unlock to access normally",
fullUnlockUrl: "Full Unlock URL",
saveThisUrl: "Please save this URL carefully",
unlockPassword: "Unlock Password",
unlockPasswordHelper: "Password needed to unlock the hide; set on first time or reset when filled",
autoHideTime: "Auto Hide Time",
autoHideTimeHelper: "Minutes without requests before auto hiding",
hideOpenApi: "Hide Open API",
hideOpenApiHelper: "Whether to hide open APIs; whether to expose /api/v1 prefixed endpoints",
hideSiteImmediately: "Hide Site Immediately",
hideImmediately: "Hide Immediately",
confirmHideSiteTitle: "Are you sure to hide the site immediately?",
confirmHideSiteContent: "After hiding, the site will be inaccessible. Please operate cautiously.",
siteHiddenSuccess: "Site has been hidden",
emailServerSettings: "Email Server Settings",
setEmailSendingServer: "Set the email sending server",
useCustomEmailServer: "Use Custom Email Server",
smtpDomain: "SMTP Domain",
pleaseEnterSmtpDomain: "Please enter SMTP domain or IP",
smtpPort: "SMTP Port",
pleaseEnterSmtpPort: "Please enter SMTP port",
username: "Username",
pleaseEnterUsername: "Please enter username",
password: "Password",
pleaseEnterPassword: "Please enter password",
qqEmailAuthCodeHelper: "If using QQ email, get an authorization code in QQ email settings as the password",
senderEmail: "Sender Email",
pleaseEnterSenderEmail: "Please enter sender email",
useSsl: "Use SSL",
sslPortNote: "SSL and non-SSL SMTP ports are different, please adjust port accordingly",
ignoreCertValidation: "Ignore Certificate Validation",
useOfficialEmailServer: "Use Official Email Server",
useOfficialEmailServerHelper: "Send emails directly using the official server to avoid complicated setup",
testReceiverEmail: "Test Receiver Email",
pleaseEnterTestReceiverEmail: "Please enter test receiver email",
saveBeforeTest: "Save before testing",
sendFailHelpDoc: "Failed to send??? ",
emailConfigHelpDoc: "Email configuration help document",
tryOfficialEmailServer: "You can also try using the official email server ↗↗↗↗↗↗↗↗",
pluginManagement: "Plugin Management",
pluginBetaWarning: "Custom plugins are in BETA and may have breaking changes in future",
pleaseSelectRecord: "Please select records first",
permissionManagement: "Permission Management",
adda: "Add",
rootNode: "Root Node",
permissionName: "Permission Name",
enterPermissionName: "Please enter permission name",
permissionCode: "Permission Code",
enterPermissionCode: "Please enter permission code",
max100Chars: "Maximum 100 characters",
examplePermissionCode: "e.g.: sys:user:view",
sortOrder: "Sort Order",
sortRequired: "Sort order is required",
parentNode: "Parent Node",
roleManagement: "Role Management",
assignPermissions: "Assign Permissions",
roleName: "Role Name",
enterRoleName: "Please enter role name",
unlockLogin: "Unlock Login",
notice: "Notice",
confirmUnlock: "Are you sure you want to unlock this user's login?",
unlockSuccess: "Unlock successful",
enterUsername: "Please enter username",
modifyPasswordIfFilled: "Fill in to change the password",
emaila: "Email",
mobile: "Mobile",
avatar: "Avatar",
validTime: "Valid Time",
remark: "Remark",
roles: "Roles",
cnameTitle: "CNAME Service Configuration",
cnameDescription:
"The domain name configured here serves as a proxy for verifying other domains. When other domains apply for certificates, they map to this domain via CNAME for ownership verification. The advantage is that any domain can apply for a certificate this way without providing an AccessSecret.",
cnameLinkText: "CNAME principle and usage instructions",
confirmTitle: "Confirm",
confirmDeleteBatch: "Are you sure you want to delete these {count} records?",
selectRecordsFirst: "Please select records first",
cnameDomain: "CNAME Domain",
cnameDomainPlaceholder: "cname.handsfree.work",
cnameDomainHelper:
"Requires a domain registered with a DNS provider on the right (or you can transfer other domain DNS servers here).\nOnce the CNAME domain is set, it cannot be changed. It is recommended to use a first-level subdomain.",
dnsProvider: "DNS Provider",
dnsProviderAuthorization: "DNS Provider Authorization",
setDefault: "Set Default",
confirmSetDefault: "Are you sure to set as default?",
setAsDefault: "Set as Default",
disabledLabel: "Disabled",
confirmToggleStatus: "Are you sure to {action}?",
}; };
@@ -453,4 +453,238 @@ export default {
subdomainManagement: "子域管理", subdomainManagement: "子域管理",
isDisabled: "是否禁用", isDisabled: "是否禁用",
enabled: "启用", enabled: "启用",
uploadCustomCert: "上传自定义证书",
sourcee: "来源",
sourcePipeline: "流水线",
sourceManualUpload: "手动上传",
domains: "域名",
enterDomain: "请输入域名",
validDays: "有效天数",
expires: "过期",
days: "天",
expireTime: "过期时间",
certIssuer: "证书颁发机构",
applyTime: "申请时间",
relatedPipeline: "关联流水线",
statusSuccess: "成功",
statusChecking: "检查中",
statusError: "异常",
actionImportBatch: "批量导入",
actionSyncIp: "同步IP",
modalTitleSyncIp: "同步IP",
modalContentSyncIp: "确定要同步IP吗?",
notificationSyncComplete: "同步完成",
actionCheckAll: "检查全部",
modalTitleConfirm: "确认",
modalContentCheckAll: "确认触发检查全部IP站点的证书吗?",
notificationCheckSubmitted: "检查任务已提交",
notificationCheckDescription: "请稍后刷新页面查看结果",
tooltipCheckNow: "立即检查",
notificationCheckSubmittedPleaseRefresh: "检查任务已提交,请稍后刷新查看结果",
columnId: "ID",
columnIp: "IP",
helperIpCname: "也支持填写CNAME域名",
ruleIpRequired: "请输入IP",
columnCertDomains: "证书域名",
columnCertProvider: "颁发机构",
columnCertStatus: "证书状态",
statusNormal: "正常",
statusExpired: "过期",
columnCertExpiresTime: "证书到期时间",
expired: "过期",
columnCheckStatus: "检查状态",
columnLastCheckTime: "上次检查时间",
columnSource: "来源",
sourceSync: "同步",
sourceManual: "手动",
sourceImport: "导入",
columnDisabled: "禁用启用",
columnRemark: "备注",
pluginFile: "插件文件",
selectPluginFile: "选择插件文件",
overrideSameName: "同名覆盖",
override: "覆盖",
noOverride: "不覆盖",
overrideHelper: "如果已有相同名称插件,直接覆盖",
importPlugin: "导入插件",
operationSuccess: "操作成功",
customPlugin: "自定义插件",
import: "导入",
export: "导出",
pluginType: "插件类型",
auth: "授权",
dns: "DNS",
deployPlugin: "部署插件",
icon: "图标",
pluginName: "插件名称",
pluginNameHelper: "必须为英文或数字,驼峰命名,类型作为前缀\n示例:AliyunDeployToCDN\n插件使用后,名称不可修改",
pluginNameRuleMsg: "必须为英文或数字,驼峰命名,类型作为前缀",
author: "作者",
authorHelper: "上传插件市场时作为前缀,如 greper/pluginName",
authorRuleMsg: "必须为英文或数字",
titleHelper: "插件中文名称",
descriptionHelper: "插件描述",
builtIn: "内置",
custom: "自定义",
store: "市场",
version: "版本",
pluginDependencies: "插件依赖",
pluginDependenciesHelper: "格式: [作者/]插件名[:版本],需先安装依赖插件",
editableRunStrategy: "可编辑运行策略",
editable: "可编辑",
notEditable: "不可编辑",
runStrategy: "运行策略",
normalRun: "正常运行",
skipOnSuccess: "成功跳过(部署任务)",
defaultRunStrategyHelper: "默认运行策略",
enableDisable: "启用/禁用",
clickToToggle: "点击切换启用/禁用",
confirmToggle: "确认要",
disable: "禁用",
enable: "启用",
pluginGroup: "插件分组",
icpRegistrationNumber: "ICP备案号",
icpPlaceholder: "粤ICP备xxxxxxx号",
publicSecurityRegistrationNumber: "网安备案号",
publicSecurityPlaceholder: "京公网安备xxxxxxx号",
enableAssistant: "开启小助手",
allowCrawlers: "允许爬虫",
httpProxy: "HTTP代理",
httpProxyPlaceholder: "http://192.168.1.2:18010/",
httpProxyHelper: "当某些网站被墙时可以配置",
httpsProxy: "HTTPS代理",
httpsProxyPlaceholder: "http://192.168.1.2:18010/",
saveThenTestTitle: "保存后,再点击测试",
testButton: "测试",
httpsProxyHelper: "一般这两个代理填一样的,保存后再测试",
dualStackNetwork: "双栈网络",
default: "默认",
ipv4Priority: "IPV4优先",
ipv6Priority: "IPV6优先",
dualStackNetworkHelper: "如果选择IPv6优先,需要在docker-compose.yaml中启用ipv6",
enableCommonCnameService: "启用公共CNAME服务",
commonCnameHelper: "是否可以使用公共CNAME服务,如果禁用,且没有设置<router-link to='/sys/cname/provider'>自定义CNAME服务</router-link>,则无法使用CNAME代理方式申请证书",
saveButton: "保存",
stopSuccess: "停止成功",
google: "Google",
baidu: "百度",
success: "成功",
testFailed: "测试失败",
testCompleted: "测试完成",
manageOtherUserPipeline: "管理其他用户流水线",
limitUserPipelineCount: "限制用户流水线数量",
limitUserPipelineCountHelper: "0为不限制",
enableSelfRegistration: "开启自助注册",
enableUserValidityPeriod: "开启用户有效期",
userValidityPeriodHelper: "有效期内用户可正常使用,失效后流水线将被停用",
enableUsernameRegistration: "开启用户名注册",
enableEmailRegistration: "开启邮箱注册",
proFeature: "专业版功能",
emailServerSetup: "设置邮箱服务器",
enableSmsLoginRegister: "开启手机号登录、注册",
commFeature: "商业版功能",
smsProvider: "短信提供商",
aliyunSms: "阿里云短信",
yfySms: "易发云短信",
smsTest: "短信测试",
testMobilePlaceholder: "输入测试手机号",
saveThenTest: "保存后再点击测试",
enterTestMobile: "请输入测试手机号",
sendSuccess: "发送成功",
atLeastOneLoginRequired: "密码登录和手机号登录至少开启一个",
fieldRequired: "此项必填",
siteHide: "站点隐藏",
enableSiteHide: "启用站点隐藏",
siteHideDescription: "可以在平时关闭站点的可访问性,需要时再打开,增强站点安全性",
helpDoc: "帮助说明",
randomAddress: "随机地址",
siteHideUrlHelper: "站点被隐藏后,需要访问此URL解锁,才能正常访问",
fullUnlockUrl: "完整解除隐藏地址",
saveThisUrl: "请保存好此地址",
unlockPassword: "解除密码",
unlockPasswordHelper: "解除隐藏时需要输入密码,第一次需要设置密码,填写则重置密码",
autoHideTime: "自动隐藏时间",
autoHideTimeHelper: "多少分钟内无请求自动隐藏",
hideOpenApi: "隐藏开放接口",
hideOpenApiHelper: "是否隐藏开放接口,是否放开/api/v1开头的接口",
hideSiteImmediately: "立即隐藏站点",
hideImmediately: "立即隐藏",
confirmHideSiteTitle: "确定要立即隐藏站点吗?",
confirmHideSiteContent: "隐藏后,将无法访问站点,请谨慎操作",
siteHiddenSuccess: "站点已隐藏",
emailServerSettings: "邮件服务器设置",
setEmailSendingServer: "设置邮件发送服务器",
useCustomEmailServer: "使用自定义邮件服务器",
smtpDomain: "SMTP域名",
pleaseEnterSmtpDomain: "请输入smtp域名或ip",
smtpPort: "SMTP端口",
pleaseEnterSmtpPort: "请输入smtp端口号",
username: "用户名",
pleaseEnterUsername: "请输入用户名",
password: "密码",
pleaseEnterPassword: "请输入密码",
qqEmailAuthCodeHelper: "如果是qq邮箱,需要到qq邮箱的设置里面申请授权码作为密码",
senderEmail: "发件邮箱",
pleaseEnterSenderEmail: "请输入发件邮箱",
useSsl: "是否ssl",
sslPortNote: "ssl和非ssl的smtp端口是不一样的,注意修改端口",
ignoreCertValidation: "忽略证书校验",
useOfficialEmailServer: "使用官方邮件服务器",
useOfficialEmailServerHelper: "使用官方邮箱服务器直接发邮件,免除繁琐的配置",
testReceiverEmail: "测试收件邮箱",
pleaseEnterTestReceiverEmail: "请输入测试收件邮箱",
saveBeforeTest: "保存后再点击测试",
sendFailHelpDoc: "发送失败???",
emailConfigHelpDoc: "邮件配置帮助文档",
tryOfficialEmailServer: "您还可以试试使用官方邮件服务器↗↗↗↗↗↗↗↗",
pluginManagement: "插件管理",
pluginBetaWarning: "自定义插件处于BETA测试版,后续可能会有破坏性变更",
pleaseSelectRecord: "请先勾选记录",
permissionManagement: "权限管理",
adda: "添加",
rootNode: "根节点",
permissionName: "权限名称",
enterPermissionName: "请输入权限名称",
permissionCode: "权限代码",
enterPermissionCode: "请输入权限代码",
max100Chars: "最大100个字符",
examplePermissionCode: "例如:sys:user:view",
sortOrder: "排序",
sortRequired: "排序号必填",
parentNode: "父节点",
roleManagement: "角色管理",
assignPermissions: "分配权限",
roleName: "角色名称",
enterRoleName: "请输入角色名称",
unlockLogin: "解除登录锁定",
notice: "提示",
confirmUnlock: "确定要解除该用户的登录锁定吗?",
unlockSuccess: "解除成功",
enterUsername: "请输入用户名",
modifyPasswordIfFilled: "填写则修改密码",
emaila: "邮箱",
mobile: "手机号",
avatar: "头像",
validTime: "有效期",
remark: "备注",
roles: "角色",
cnameTitle: "CNAME服务配置",
cnameDescription:
"此处配置的域名作为其他域名校验的代理,当别的域名需要申请证书时,通过CNAME映射到此域名上来验证所有权。好处是任何域名都可以通过此方式申请证书,也无需填写AccessSecret。",
cnameLinkText: "CNAME功能原理及使用说明",
confirmTitle: "确认",
confirmDeleteBatch: "确定要批量删除这{count}条记录吗",
selectRecordsFirst: "请先勾选记录",
cnameDomain: "CNAME域名",
cnameDomainPlaceholder: "cname.handsfree.work",
cnameDomainHelper:
"需要一个右边DNS提供商注册的域名(也可以将其他域名的dns服务器转移到这几家来)。\nCNAME域名一旦确定不可修改,建议使用一级子域名",
dnsProvider: "DNS提供商",
dnsProviderAuthorization: "DNS提供商授权",
setDefault: "设置默认",
confirmSetDefault: "确定要设置为默认吗?",
setAsDefault: "设为默认",
disabledLabel: "禁用",
confirmToggleStatus: "确定要{action}吗?",
}; };
@@ -82,7 +82,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
show: true, show: true,
buttons: { buttons: {
add: { add: {
text: "上传自定义证书", text: t('certd.uploadCustomCert'),
type: "primary", type: "primary",
show: false, show: false,
async click() { async click() {
@@ -150,15 +150,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
}, },
fromType: { fromType: {
title: "来源", title: t('certd.sourcee'),
search: { search: {
show: true, show: true,
}, },
type: "dict-select", type: "dict-select",
dict: dict({ dict: dict({
data: [ data: [
{ label: "流水线", value: "pipeline" }, { label: t('certd.sourcePipeline'), value: "pipeline" },
{ label: "手动上传", value: "upload" }, { label: t('certd.sourceManualUpload'), value: "upload" },
], ],
}), }),
form: { form: {
@@ -179,13 +179,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
}, },
domains: { domains: {
title: "域名", title: t('certd.domains'),
search: { search: {
show: true, show: true,
}, },
type: "text", type: "text",
form: { form: {
rules: [{ required: true, message: "请输入域名" }], rules: [{ required: true, message: t('certd.enterDomain') }],
}, },
column: { column: {
width: 450, width: 450,
@@ -197,7 +197,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
}, },
domainCount: { domainCount: {
title: "域名数量", title: t('certd.domainCount'),
type: "number", type: "number",
form: { form: {
show: false, show: false,
@@ -209,7 +209,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
}, },
expiresLeft: { expiresLeft: {
title: "有效天数", title: t('certd.validDays'),
search: { search: {
show: false, show: false,
}, },
@@ -229,12 +229,12 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const leftDays = dayjs(value).diff(dayjs(), "day"); const leftDays = dayjs(value).diff(dayjs(), "day");
const color = leftDays < 20 ? "red" : "#389e0d"; const color = leftDays < 20 ? "red" : "#389e0d";
const percent = (leftDays / 90) * 100; const percent = (leftDays / 90) * 100;
return <a-progress title={expireDate + "过期"} percent={percent} strokeColor={color} format={(percent: number) => `${leftDays}`} />; return <a-progress title={expireDate + t('certd.expires')} percent={percent} strokeColor={color} format={(percent: number) => `${leftDays}${t('certd.days')}`} />;
}, },
}, },
}, },
expiresTime: { expiresTime: {
title: "过期时间", title: t('certd.expireTime'),
search: { search: {
show: false, show: false,
}, },
@@ -247,7 +247,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
}, },
certProvider: { certProvider: {
title: "证书颁发机构", title: t('certd.certIssuer'),
search: { search: {
show: false, show: false,
}, },
@@ -260,7 +260,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
}, },
applyTime: { applyTime: {
title: "申请时间", title: t('certd.applyTime'),
search: { search: {
show: false, show: false,
}, },
@@ -273,7 +273,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
}, },
"pipeline.title": { "pipeline.title": {
title: "关联流水线", title: t('certd.relatedPipeline'),
search: { show: false }, search: { show: false },
type: "link", type: "link",
form: { form: {
@@ -291,6 +291,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
}, },
}, },
}, },
}, },
}; };
@@ -7,353 +7,354 @@ import { Modal, notification } from "ant-design-vue";
import { useSiteIpMonitor } from "/@/views/certd/monitor/site/ip/use"; import { useSiteIpMonitor } from "/@/views/certd/monitor/site/ip/use";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const api = siteIpApi; const { t } = useI18n();
const api = siteIpApi;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => { const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
if (!query.query) { if (!query.query) {
query.query = {}; query.query = {};
} }
query.query.siteId = context.props.siteId; query.query.siteId = context.props.siteId;
return await api.GetList(query); return await api.GetList(query);
}; };
const editRequest = async (req: EditReq) => { const editRequest = async (req: EditReq) => {
const { form, row } = req; const { form, row } = req;
form.id = row.id; form.id = row.id;
const res = await api.UpdateObj(form); const res = await api.UpdateObj(form);
return res; return res;
}; };
const delRequest = async (req: DelReq) => { const delRequest = async (req: DelReq) => {
const { row } = req; const { row } = req;
return await api.DelObj(row.id); return await api.DelObj(row.id);
}; };
const addRequest = async (req: AddReq) => { const addRequest = async (req: AddReq) => {
const { form } = req; const { form } = req;
form.siteId = context.props.siteId; form.siteId = context.props.siteId;
const res = await api.AddObj(form); const res = await api.AddObj(form);
return res; return res;
}; };
const checkStatusDict = dict({ const checkStatusDict = dict({
data: [ data: [
{ label: "成功", value: "ok", color: "green" }, { label: t("certd.statusSuccess"), value: "ok", color: "green" },
{ label: "检查中", value: "checking", color: "blue" }, { label: t("certd.statusChecking"), value: "checking", color: "blue" },
{ label: "异常", value: "error", color: "red" }, { label: t("certd.statusError"), value: "error", color: "red" },
], ],
}); });
const { openSiteIpImportDialog } = useSiteIpMonitor(); const { openSiteIpImportDialog } = useSiteIpMonitor();
return { return {
crudOptions: { crudOptions: {
request: { request: {
pageRequest, pageRequest,
addRequest, addRequest,
editRequest, editRequest,
delRequest, delRequest,
}, },
form: { form: {
labelCol: { labelCol: {
//固定label宽度 //固定label宽度
span: null, span: null,
style: { style: {
width: "100px", width: "100px",
}, },
}, },
col: { col: {
span: 22, span: 22,
}, },
wrapper: { wrapper: {
width: 600, width: 600,
}, },
}, },
actionbar: { actionbar: {
buttons: { buttons: {
add: { add: {
async click() { async click() {
await crudExpose.openAdd({}); await crudExpose.openAdd({});
}, },
}, },
import: { import: {
show: true, show: true,
text: "批量导入", text: t("certd.actionImportBatch"),
type: "primary", type: "primary",
async click() { async click() {
openSiteIpImportDialog({ openSiteIpImportDialog({
siteId: context.props.siteId, siteId: context.props.siteId,
afterSubmit() { afterSubmit() {
crudExpose.doRefresh(); crudExpose.doRefresh();
}, },
}); });
}, },
}, },
load: { load: {
text: "同步IP", text: t("certd.actionSyncIp"),
type: "primary", type: "primary",
async click() { async click() {
Modal.confirm({ Modal.confirm({
title: "同步IP", title: t("certd.modalTitleSyncIp"),
content: "确定要同步IP吗?", content: t("certd.modalContentSyncIp"),
onOk: async () => { onOk: async () => {
await api.DoSync(context.props.siteId); await api.DoSync(context.props.siteId);
await crudExpose.doRefresh(); await crudExpose.doRefresh();
notification.success({ notification.success({
message: "同步完成", message: t("certd.notificationSyncComplete"),
}); });
}, },
}); });
}, },
}, },
checkAll: { checkAll: {
text: "检查全部", text: t("certd.actionCheckAll"),
type: "primary", type: "primary",
click: () => { click: () => {
Modal.confirm({ Modal.confirm({
title: "确认", title: t("certd.modalTitleConfirm"),
content: "确认触发检查全部IP站点的证书吗?", content: t("certd.modalContentCheckAll"),
onOk: async () => { onOk: async () => {
await siteIpApi.CheckAll(context.props.siteId); await siteIpApi.CheckAll(context.props.siteId);
notification.success({ notification.success({
message: "检查任务已提交", message: t("certd.notificationCheckSubmitted"),
description: "请稍后刷新页面查看结果", description: t("certd.notificationCheckDescription"),
}); });
}, },
}); });
}, },
}, },
}, },
}, },
rowHandle: { rowHandle: {
fixed: "right", fixed: "right",
width: 240, width: 240,
buttons: { buttons: {
check: { check: {
order: 0, order: 0,
type: "link", type: "link",
text: null, text: null,
tooltip: { tooltip: {
title: "立即检查", title: t("certd.tooltipCheckNow"),
}, },
icon: "ion:play-sharp", icon: "ion:play-sharp",
click: async ({ row }) => { click: async ({ row }) => {
await api.DoCheck(row.id); await api.DoCheck(row.id);
await crudExpose.doRefresh(); await crudExpose.doRefresh();
notification.success({ notification.success({
message: "检查任务已提交,请稍后刷新查看结果", message: t("certd.notificationCheckSubmittedPleaseRefresh"),
}); });
}, },
}, },
}, },
}, },
columns: { columns: {
id: { id: {
title: "ID", title: t("certd.columnId"),
key: "id", key: "id",
type: "number", type: "number",
search: { search: {
show: false, show: false,
}, },
column: { column: {
width: 80, width: 80,
align: "center", align: "center",
}, },
form: { form: {
show: false, show: false,
}, },
}, },
ipAddress: { ipAddress: {
title: "IP", title: t("certd.columnIp"),
search: { search: {
show: true, show: true,
}, },
type: "text", type: "text",
helper: "也支持填写CNAME域名", helper: t("certd.helperIpCname"),
form: { form: {
rules: [{ required: true, message: "请输入IP" }], rules: [{ required: true, message: t("certd.ruleIpRequired") }],
}, },
column: { column: {
width: 160, width: 160,
}, },
}, },
certDomains: { certDomains: {
title: "证书域名", title: t("certd.columnCertDomains"),
search: { search: {
show: false, show: false,
}, },
type: "text", type: "text",
form: { form: {
show: false, show: false,
}, },
column: { column: {
width: 200, width: 200,
sorter: true, sorter: true,
show: false, show: false,
cellRender({ value }) { cellRender({ value }) {
return ( return (
<a-tooltip title={value} placement="left"> <a-tooltip title={value} placement="left">
{value} {value}
</a-tooltip> </a-tooltip>
); );
}, },
}, },
}, },
certProvider: { certProvider: {
title: "颁发机构", title: t("certd.columnCertProvider"),
search: { search: {
show: false, show: false,
}, },
type: "text", type: "text",
form: { form: {
show: false, show: false,
}, },
column: { column: {
width: 200, width: 200,
show: false, show: false,
sorter: true, sorter: true,
cellRender({ value }) { cellRender({ value }) {
return <a-tooltip title={value}>{value}</a-tooltip>; return <a-tooltip title={value}>{value}</a-tooltip>;
}, },
}, },
}, },
certStatus: { certStatus: {
title: "证书状态", title: t("certd.columnCertStatus"),
search: { search: {
show: true, show: true,
}, },
type: "dict-select", type: "dict-select",
dict: dict({ dict: dict({
data: [ data: [
{ label: "正常", value: "ok", color: "green" }, { label: t("certd.statusNormal"), value: "ok", color: "green" },
{ label: "过期", value: "expired", color: "red" }, { label: t("certd.statusExpired"), value: "expired", color: "red" },
], ],
}), }),
form: { form: {
show: false, show: false,
}, },
column: { column: {
width: 100, width: 100,
sorter: true, sorter: true,
show: true, show: true,
align: "center", align: "center",
}, },
}, },
certExpiresTime: { certExpiresTime: {
title: "证书到期时间", title: t("certd.columnCertExpiresTime"),
search: { search: {
show: false, show: false,
}, },
type: "date", type: "date",
form: { form: {
show: false, show: false,
}, },
column: { column: {
sorter: true, sorter: true,
cellRender({ value }) { cellRender({ value }) {
if (!value) { if (!value) {
return "-"; return "-";
} }
const expireDate = dayjs(value).format("YYYY-MM-DD"); const expireDate = dayjs(value).format("YYYY-MM-DD");
const leftDays = dayjs(value).diff(dayjs(), "day"); const leftDays = dayjs(value).diff(dayjs(), "day");
const color = leftDays < 20 ? "red" : "#389e0d"; const color = leftDays < 20 ? "red" : "#389e0d";
const percent = (leftDays / 90) * 100; const percent = (leftDays / 90) * 100;
return <a-progress title={expireDate + "过期"} percent={percent} strokeColor={color} format={(percent: number) => `${leftDays}`} />; return <a-progress title={expireDate + " " + t("certd.expired")} percent={percent} strokeColor={color} format={(percent: number) => `${leftDays} ${t("certd.days")}`} />;
}, },
}, },
}, },
checkStatus: { checkStatus: {
title: "检查状态", title: t("certd.columnCheckStatus"),
search: { search: {
show: false, show: false,
}, },
type: "dict-select", type: "dict-select",
dict: checkStatusDict, dict: checkStatusDict,
form: { form: {
show: false, show: false,
}, },
column: { column: {
width: 100, width: 100,
align: "center", align: "center",
sorter: true, sorter: true,
cellRender({ value, row, key }) { cellRender({ value, row, key }) {
return ( return (
<a-tooltip title={row.error}> <a-tooltip title={row.error}>
<fs-values-format v-model={value} dict={checkStatusDict}></fs-values-format> <fs-values-format v-model={value} dict={checkStatusDict}></fs-values-format>
</a-tooltip> </a-tooltip>
); );
}, },
}, },
}, },
lastCheckTime: { lastCheckTime: {
title: "上次检查时间", title: t("certd.columnLastCheckTime"),
search: { search: {
show: false, show: false,
}, },
type: "datetime", type: "datetime",
form: { form: {
show: false, show: false,
}, },
column: { column: {
sorter: true, sorter: true,
width: 155, width: 155,
}, },
}, },
from: { from: {
title: "来源", title: t("certd.columnSource"),
search: { search: {
show: false, show: false,
}, },
type: "dict-switch", type: "dict-switch",
dict: dict({ dict: dict({
data: [ data: [
{ label: "同步", value: "sync", color: "green" }, { label: t("certd.sourceSync"), value: "sync", color: "green" },
{ label: "手动", value: "manual", color: "blue" }, { label: t("certd.sourceManual"), value: "manual", color: "blue" },
{ label: "导入", value: "import", color: "blue" }, { label: t("certd.sourceImport"), value: "import", color: "blue" },
], ],
}), }),
form: { form: {
value: false, value: false,
}, },
column: { column: {
width: 100, width: 100,
sorter: true, sorter: true,
align: "center", align: "center",
}, },
}, },
disabled: { disabled: {
title: "禁用启用", title: t("certd.columnDisabled"),
search: { search: {
show: false, show: false,
}, },
type: "dict-switch", type: "dict-switch",
dict: dict({ dict: dict({
data: [ data: [
{ label: "启用", value: false, color: "green" }, { label: t("certd.enabled"), value: false, color: "green" },
{ label: "禁用", value: true, color: "red" }, { label: t("certd.disabled"), value: true, color: "red" },
], ],
}), }),
form: { form: {
value: false, value: false,
}, },
column: { column: {
width: 100, width: 100,
sorter: true, sorter: true,
align: "center", align: "center",
}, },
}, },
remark: { remark: {
title: "备注", title: t("certd.columnRemark"),
search: { search: {
show: false, show: false,
}, },
type: "text", type: "text",
form: { form: {
show: false, show: false,
}, },
column: { column: {
width: 200, width: 200,
sorter: true, sorter: true,
tooltip: true, tooltip: true,
}, },
}, },
}, },
}, },
}; };
} }
@@ -1,150 +1,152 @@
import * as api from "./api.js"; import * as api from "./api.js";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud"; import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet { export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => { const { t } = useI18n();
const list = await api.GetTree(); const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
const list = await api.GetTree();
return { return {
offset: 0, offset: 0,
records: list, records: list,
total: 10000, total: 10000,
limit: 10000 limit: 10000
}; };
}; };
async function afterChange() { async function afterChange() {
await permissionTreeDict.reloadDict(); await permissionTreeDict.reloadDict();
} }
const editRequest = async ({ form, row }: EditReq) => { const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id; form.id = row.id;
const ret = await api.UpdateObj(form); const ret = await api.UpdateObj(form);
await afterChange(); await afterChange();
return ret; return ret;
}; };
const delRequest = async ({ row }: DelReq) => { const delRequest = async ({ row }: DelReq) => {
const ret = await api.DelObj(row.id); const ret = await api.DelObj(row.id);
await afterChange(); await afterChange();
return ret; return ret;
}; };
const addRequest = async ({ form }: AddReq) => { const addRequest = async ({ form }: AddReq) => {
const ret = await api.AddObj(form); const ret = await api.AddObj(form);
await afterChange(); await afterChange();
return ret; return ret;
}; };
const permissionTreeDict = dict({ const permissionTreeDict = dict({
url: "/sys/authority/permission/tree", url: "/sys/authority/permission/tree",
isTree: true, isTree: true,
value: "id", value: "id",
label: "title", label: "title",
async onReady({ dict }: any) { async onReady({ dict }: any) {
dict.setData([{ id: -1, title: "根节点", children: dict.data }]); dict.setData([{ id: -1, title: t("certd.rootNode"), children: dict.data }]);
} }
}); });
return { return {
crudOptions: { crudOptions: {
request: { request: {
pageRequest, pageRequest,
addRequest, addRequest,
editRequest, editRequest,
delRequest delRequest
}, },
actionbar: { actionbar: {
show: false show: false
}, },
toolbar: { toolbar: {
show: false show: false
}, },
table: { table: {
show: false show: false
// scroll: { fixed: true } // scroll: { fixed: true }
}, },
rowHandle: { rowHandle: {
fixed: "right" fixed: "right"
}, },
search: { search: {
show: false show: false
}, },
pagination: { pagination: {
show: false, show: false,
pageSize: 100000 pageSize: 100000
}, },
columns: { columns: {
id: { id: {
title: "id", title: "id",
type: "number", type: "number",
form: { show: false }, // 表单配置 form: { show: false }, // 表单配置
column: { column: {
width: 120, width: 120,
sortable: "custom" sortable: "custom"
} }
}, },
title: { title: {
title: "权限名称", title: t("certd.permissionName"),
type: "text", type: "text",
form: { form: {
rules: [ rules: [
{ required: true, message: "请输入权限名称" }, { required: true, message: t("certd.enterPermissionName") },
{ max: 50, message: "最大50个字符" } { max: 50, message: t("certd.max50Chars") }
], ],
component: { component: {
placeholder: "权限名称" placeholder: t("certd.permissionName")
} }
}, },
column: { column: {
width: 200 width: 200
} }
}, },
permission: {
title: t("certd.permissionCode"),
type: "text",
column: {
width: 170
},
form: {
rules: [
{ required: true, message: t("certd.enterPermissionCode") },
{ max: 100, message: t("certd.max100Chars") }
],
component: {
placeholder: t("certd.examplePermissionCode")
}
}
},
sort: {
title: t("certd.sortOrder"),
type: "number",
column: {
width: 100
},
form: {
value: 100,
rules: [{ required: true, type: "number", message: t("certd.sortRequired") }]
}
},
parentId: {
title: t("certd.parentNode"),
type: "dict-tree",
column: {
width: 100
},
dict: permissionTreeDict,
form: {
value: -1,
component: {
multiple: false,
defaultExpandAll: true,
dict: { cache: false },
fieldNames: {
value: "id",
label: "title"
}
}
}
}
permission: { }
title: "权限代码", }
type: "text", };
column: {
width: 170
},
form: {
rules: [
{ required: true, message: "请输入权限代码" },
{ max: 100, message: "最大100个字符" }
],
component: {
placeholder: "例如:sys:user:view"
}
}
},
sort: {
title: "排序",
type: "number",
column: {
width: 100
},
form: {
value: 100,
rules: [{ required: true, type: "number", message: "排序号必填" }]
}
},
parentId: {
title: "父节点",
type: "dict-tree",
column: {
width: 100
},
dict: permissionTreeDict,
form: {
value: -1,
component: {
multiple: false,
defaultExpandAll: true,
dict: { cache: false },
fieldNames: {
value: "id",
label: "title"
}
}
}
}
}
}
};
} }
@@ -1,193 +1,194 @@
<template> <template>
<a-tree <a-tree v-if="computedTree" ref="treeRef" class="fs-permission-tree" :class="{ 'is-editable': editable }"
v-if="computedTree" :selectable="false" show-line :show-icon="false" :default-expand-all="true" :tree-data="computedTree"
ref="treeRef" @check="onChecked">
class="fs-permission-tree" <template #title="scope">
:class="{ 'is-editable': editable }" <div class="node-title-pane">
:selectable="false" <div class="node-title">{{ scope.title }}</div>
show-line <div v-if="editable === true" class="node-suffix">
:show-icon="false" <fs-icon v-if="actions.add !== false" :icon="$fsui.icons.add" @click.stop="add(scope)" />
:default-expand-all="true" <fs-icon v-if="actions.edit !== false && scope.id !== -1" :icon="$fsui.icons.edit"
:tree-data="computedTree" @click.stop="edit(scope)" />
@check="onChecked" <fs-icon v-if="actions.remove !== false && scope.id !== -1" :icon="$fsui.icons.remove"
> @click.stop="remove(scope)" />
<template #title="scope"> </div>
<div class="node-title-pane"> </div>
<div class="node-title">{{ scope.title }}</div> </template>
<div v-if="editable === true" class="node-suffix"> </a-tree>
<fs-icon v-if="actions.add !== false" :icon="$fsui.icons.add" @click.stop="add(scope)" />
<fs-icon v-if="actions.edit !== false && scope.id !== -1" :icon="$fsui.icons.edit" @click.stop="edit(scope)" />
<fs-icon v-if="actions.remove !== false && scope.id !== -1" :icon="$fsui.icons.remove" @click.stop="remove(scope)" />
</div>
</div>
</template>
</a-tree>
</template> </template>
<script lang="ts"> <script lang="ts">
import { utils } from "@fast-crud/fast-crud"; import { utils } from "@fast-crud/fast-crud";
import { cloneDeep } from "lodash-es"; import { cloneDeep } from "lodash-es";
import { computed, defineComponent, ref } from "vue"; import { computed, defineComponent, ref } from "vue";
import { useI18n } from "vue-i18n";
export default defineComponent({ export default defineComponent({
name: "FsPermissionTree", name: "FsPermissionTree",
props: { props: {
/** /**
* 树形数据 * 树形数据
* */ * */
tree: {}, tree: {},
/** /**
* 是否可编辑 * 是否可编辑
*/ */
editable: { editable: {
default: true default: true
}, },
actions: { actions: {
default: {} default: {}
} }
} as any, } as any,
emits: ["add", "edit", "remove"], emits: ["add", "edit", "remove"],
setup(props: any, ctx) { setup(props: any, ctx) {
const treeRef = ref(); const { t } = useI18n();
const computedTree = computed(() => { const treeRef = ref();
if (props.tree == null) { const computedTree = computed(() => {
return null; if (props.tree == null) {
} return null;
const clone = cloneDeep(props.tree); }
utils.deepdash.forEachDeep(clone, (value: any, key: any, pNode: any, context: any) => { const clone = cloneDeep(props.tree);
if (value == null) { utils.deepdash.forEachDeep(clone, (value: any, key: any, pNode: any, context: any) => {
return; if (value == null) {
} return;
if (!(value instanceof Object) || value instanceof Array) { }
return; if (!(value instanceof Object) || value instanceof Array) {
} return;
if (value.class === "is-leaf") { }
//处理过,无需再次处理 if (value.class === "is-leaf") {
return; //处理过,无需再次处理
} return;
value.class = "is-twig"; }
if (value.children != null && value.children.length > 0) { value.class = "is-twig";
return; if (value.children != null && value.children.length > 0) {
} return;
const parents = context.parents; }
if (parents.length < 2) { const parents = context.parents;
return; if (parents.length < 2) {
} return;
const parent = parents[parents.length - 2].value; }
//看parent下面的children,是否全部都没有children const parent = parents[parents.length - 2].value;
for (const child of parent.children) { //看parent下面的children,是否全部都没有children
if (child.children != null && child.children.length > 0) { for (const child of parent.children) {
//存在child有children if (child.children != null && child.children.length > 0) {
return; //存在child有children
} return;
} }
// 所有的子节点都没有children }
parent.class = "is-twig"; // 连接叶子节点的末梢枝杈节点 // 所有的子节点都没有children
let i = 0; parent.class = "is-twig"; // 连接叶子节点的末梢枝杈节点
for (const child of parent.children) { let i = 0;
child.class = "is-leaf"; for (const child of parent.children) {
if (i !== 0) { child.class = "is-leaf";
child.class += " leaf-after"; if (i !== 0) {
} child.class += " leaf-after";
i++; }
} i++;
}); }
return [ });
{ return [
title: "根节点", {
id: -1, title: t("certd.rootNode"),
children: clone id: -1,
} children: clone
]; }
}); ];
function add(scope: any) { });
ctx.emit("add", scope.dataRef); function add(scope: any) {
} ctx.emit("add", scope.dataRef);
function edit(scope: any) { }
ctx.emit("edit", scope.dataRef); function edit(scope: any) {
} ctx.emit("edit", scope.dataRef);
function remove(scope: any) { }
ctx.emit("remove", scope.dataRef); function remove(scope: any) {
} ctx.emit("remove", scope.dataRef);
function onChecked(a: any, b: any, c: any) { }
utils.logger.info("chedcked", a, b, c); function onChecked(a: any, b: any, c: any) {
} utils.logger.info("chedcked", a, b, c);
function getChecked() { }
const checked = treeRef.value.checkedKeys; function getChecked() {
const halfChecked = treeRef.value.halfCheckedKeys; const checked = treeRef.value.checkedKeys;
return { const halfChecked = treeRef.value.halfCheckedKeys;
checked, return {
halfChecked checked,
}; halfChecked
} };
return { }
computedTree, return {
add, computedTree,
edit, add,
remove, edit,
treeRef, remove,
onChecked, treeRef,
getChecked onChecked,
}; getChecked,
} t
};
}
}); });
</script> </script>
<style lang="less"> <style lang="less">
.fs-permission-tree { .fs-permission-tree {
.ant-tree-list-holder-inner { .ant-tree-list-holder-inner {
flex-direction: row !important; flex-direction: row !important;
flex-wrap: wrap; flex-wrap: wrap;
.is-twig {
width: 100%;
}
.is-leaf { .is-twig {
//border-bottom: 1px solid #ddd; width: 100%;
&::before { }
display: none;
}
&.leaf-after { .is-leaf {
.ant-tree-indent-unit {
display: none;
}
}
.node-title-pane { //border-bottom: 1px solid #ddd;
border-bottom: 1px solid #ddd; &::before {
} display: none;
} }
}
//.is-twig ul {
// display: flex;
// flex-wrap: wrap;
//}
.node-title-pane {
display: flex;
.node-title {
width: 110px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&.is-editable { &.leaf-after {
.ant-tree-title { .ant-tree-indent-unit {
&:hover { display: none;
.node-suffix { }
visibility: visible; }
}
}
}
.node-suffix { .node-title-pane {
visibility: hidden; border-bottom: 1px solid #ddd;
> * { }
margin-left: 3px; }
} }
}
} //.is-twig ul {
// display: flex;
// flex-wrap: wrap;
//}
.node-title-pane {
display: flex;
.node-title {
width: 110px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&.is-editable {
.ant-tree-title {
&:hover {
.node-suffix {
visibility: visible;
}
}
}
.node-suffix {
visibility: hidden;
>* {
margin-left: 3px;
}
}
}
} }
</style> </style>
@@ -1,76 +1,81 @@
<template> <template>
<fs-page> <fs-page>
<template #header> <template #header>
<div class="title">权限管理</div> <div class="title">{{ t("certd.permissionManagement") }}</div>
</template> </template>
<fs-crud ref="crudRef" v-bind="crudBinding"> <fs-crud ref="crudRef" v-bind="crudBinding">
<a-button v-permission="'1sys:auth:per:add'" style="margin-left: 20px" @click="addHandle({})"> <a-button v-permission="'1sys:auth:per:add'" style="margin-left: 20px" @click="addHandle({})">
<fs-icon :icon="ui.icons.add"></fs-icon> <fs-icon :icon="ui.icons.add"></fs-icon>
添加 {{ t("certd.adda") }}
</a-button> </a-button>
<fs-permission-tree class="permission-tree mt-10" :tree="crudBinding.data" :checkable="false" :actions="permission" @add="addHandle" @edit="editHandle" @remove="removeHandle"></fs-permission-tree> <fs-permission-tree class="permission-tree mt-10" :tree="crudBinding.data" :checkable="false"
</fs-crud> :actions="permission" @add="addHandle" @edit="editHandle" @remove="removeHandle"></fs-permission-tree>
</fs-page> </fs-crud>
</fs-page>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, onActivated, onMounted, ref } from "vue"; import { defineComponent, onActivated, onMounted, ref } from "vue";
import createCrudOptions from "./crud.js"; import createCrudOptions from "./crud.js";
import FsPermissionTree from "./fs-permission-tree.vue"; import FsPermissionTree from "./fs-permission-tree.vue";
import { usePermission } from "/src/plugin/permission"; import { usePermission } from "/src/plugin/permission";
import { useFs, useUi } from "@fast-crud/fast-crud"; import { useFs, useUi } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
export default defineComponent({ export default defineComponent({
name: "AuthorityManager", name: "AuthorityManager",
components: { FsPermissionTree }, components: { FsPermissionTree },
setup() { setup() {
// 此处传入permission进行通用按钮权限设置,会通过commonOptions去设置actionbar和rowHandle的按钮的show属性 // 此处传入permission进行通用按钮权限设置,会通过commonOptions去设置actionbar和rowHandle的按钮的show属性
// 更多关于按钮权限的源代码设置,请参考 ./src/plugin/fast-crud/index.js 75-77行) // 更多关于按钮权限的源代码设置,请参考 ./src/plugin/fast-crud/index.js 75-77行)
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { permission: "sys:auth:per" } }); const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { permission: "sys:auth:per" } });
const { t } = useI18n();
// 页面打开后获取列表数据 // 页面打开后获取列表数据
onMounted(async () => { onMounted(async () => {
await crudExpose.doRefresh(); await crudExpose.doRefresh();
}); });
onActivated(async () => { onActivated(async () => {
await crudExpose.doRefresh(); await crudExpose.doRefresh();
}); });
const { ui } = useUi(); const { ui } = useUi();
//用户业务代码 //用户业务代码
async function addHandle(item: any) { async function addHandle(item: any) {
await crudExpose.openAdd({ row: { parentId: item?.id ?? -1 } }); await crudExpose.openAdd({ row: { parentId: item?.id ?? -1 } });
} }
async function editHandle(item: any) { async function editHandle(item: any) {
await crudExpose.openEdit({ row: item }); await crudExpose.openEdit({ row: item });
} }
async function removeHandle(item: any) { async function removeHandle(item: any) {
await crudExpose.doRemove({ row: { id: item.id }, index: null }); await crudExpose.doRemove({ row: { id: item.id }, index: null });
} }
const { hasPermissions } = usePermission(); const { hasPermissions } = usePermission();
const permission = ref({ const permission = ref({
add: hasPermissions("1sys:auth:per:add"), add: hasPermissions("1sys:auth:per:add"),
edit: hasPermissions("1sys:auth:per:edit"), edit: hasPermissions("1sys:auth:per:edit"),
remove: hasPermissions("1sys:auth:per:remove"), remove: hasPermissions("1sys:auth:per:remove"),
}); });
return { return {
ui, ui,
crudBinding, crudBinding,
crudRef, crudRef,
addHandle, addHandle,
editHandle, editHandle,
removeHandle, removeHandle,
permission, permission,
}; t
}, };
},
}); });
</script> </script>
<style lang="less"> <style lang="less">
.permission-tree { .permission-tree {
margin-left: 20px; margin-left: 20px;
} }
</style> </style>
@@ -1,84 +1,86 @@
import * as api from "./api"; import * as api from "./api";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud"; import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
export default function ({ crudExpose, context: { authz } }: CreateCrudOptionsProps): CreateCrudOptionsRet { export default function ({ crudExpose, context: { authz } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => { const { t } = useI18n();
return await api.GetList(query); const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
}; return await api.GetList(query);
const editRequest = async ({ form, row }: EditReq) => { };
form.id = row.id; const editRequest = async ({ form, row }: EditReq) => {
return await api.UpdateObj(form); form.id = row.id;
}; return await api.UpdateObj(form);
const delRequest = async ({ row }: DelReq) => { };
return await api.DelObj(row.id); const delRequest = async ({ row }: DelReq) => {
}; return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => { const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form); return await api.AddObj(form);
}; };
return { return {
crudOptions: { crudOptions: {
request: { request: {
pageRequest, pageRequest,
addRequest, addRequest,
editRequest, editRequest,
delRequest delRequest
}, },
rowHandle: { rowHandle: {
width: 300, width: 300,
buttons: { buttons: {
authz: { authz: {
type: "link", type: "link",
text: "授权", text: "授权",
async click(context) { async click(context) {
await authz.authzOpen(context.record.id); await authz.authzOpen(context.record.id);
} }
} }
} }
}, },
columns: { columns: {
id: { id: {
title: "id", title: "id",
type: "text", type: "text",
form: { show: false }, // 表单配置 form: { show: false }, // 表单配置
column: { column: {
width: 70, width: 70,
sorter: true sorter: true
} }
}, },
name: { name: {
title: "角色名称", title: t("certd.roleName"),
type: "text", type: "text",
search: { show: true }, search: { show: true },
form: { form: {
rules: [ rules: [
{ required: true, message: "请输入角色名称" }, { required: true, message: t("certd.enterRoleName") },
{ max: 50, message: "最大50个字符" } { max: 50, message: t("certd.max50Chars") }
] ]
}, // 表单配置 }, // 表单配置
column: { column: {
sorter: true sorter: true
} }
}, },
createTime: { createTime: {
title: "创建时间", title: t("certd.createTime"),
type: "datetime", type: "datetime",
column: { column: {
sorter: true sorter: true
}, },
form: { form: {
show: false show: false
} }
}, },
updateTime: { updateTime: {
title: "更新时间", title: t("certd.updateTime"),
type: "datetime", type: "datetime",
column: { column: {
sorter: true sorter: true
}, },
form: { show: false } // 表单配置 form: { show: false } // 表单配置
} }
} }
} }
}; };
} }
@@ -1,15 +1,18 @@
<template> <template>
<fs-page> <fs-page>
<template #header> <template #header>
<div class="title">角色管理</div> <div class="title">{{ t("certd.roleManagement") }}</div>
</template> </template>
<fs-crud ref="crudRef" v-bind="crudBinding" /> <fs-crud ref="crudRef" v-bind="crudBinding" />
<a-modal v-model:open="authzDialogVisible" width="860px" title="分配权限" @ok="updatePermission"> <a-modal v-model:open="authzDialogVisible" width="860px" :title="t('certd.assignPermissions')"
<fs-permission-tree ref="permissionTreeRef" v-model:checked-keys="checkedKeys" :tree="permissionTreeData" :editable="false" checkable :replace-fields="{ key: 'id', label: 'title' }"> </fs-permission-tree> @ok="updatePermission">
</a-modal> <fs-permission-tree ref="permissionTreeRef" v-model:checked-keys="checkedKeys" :tree="permissionTreeData"
</fs-page> :editable="false" checkable :replace-fields="{ key: 'id', label: 'title' }"> </fs-permission-tree>
</a-modal>
</fs-page>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, onActivated, onMounted, ref } from "vue"; import { defineComponent, onActivated, onMounted, ref } from "vue";
import { useFs } from "@fast-crud/fast-crud"; import { useFs } from "@fast-crud/fast-crud";
@@ -19,103 +22,106 @@ import * as api from "./api";
import { message } from "ant-design-vue"; import { message } from "ant-design-vue";
import FsPermissionTree from "../permission/fs-permission-tree.vue"; import FsPermissionTree from "../permission/fs-permission-tree.vue";
import { UseCrudPermissionCompProps, UseCrudPermissionExtraProps } from "/@/plugin/permission"; import { UseCrudPermissionCompProps, UseCrudPermissionExtraProps } from "/@/plugin/permission";
import { useI18n } from "vue-i18n";
function useAuthz() { function useAuthz() {
const checkedKeys = ref(); const checkedKeys = ref();
const permissionTreeData = ref(); const permissionTreeData = ref();
const permissionTreeRef = ref(); const permissionTreeRef = ref();
const authzDialogVisible = ref(false); const authzDialogVisible = ref(false);
const currentRoleId = ref(); const currentRoleId = ref();
// 如果勾选节点中存在非叶子节点,tree组件会将其所有子节点全部勾选 // 如果勾选节点中存在非叶子节点,tree组件会将其所有子节点全部勾选
// 所以要找出所有叶子节点,仅勾选叶子节点,tree组件会将父节点同步勾选 // 所以要找出所有叶子节点,仅勾选叶子节点,tree组件会将父节点同步勾选
function getAllCheckedLeafNodeId(tree: any, checkedIds: any, temp: any) { function getAllCheckedLeafNodeId(tree: any, checkedIds: any, temp: any) {
for (let i = 0; i < tree.length; i++) { for (let i = 0; i < tree.length; i++) {
const item = tree[i]; const item = tree[i];
if (item.children && item.children.length !== 0) { if (item.children && item.children.length !== 0) {
getAllCheckedLeafNodeId(item.children, checkedIds, temp); getAllCheckedLeafNodeId(item.children, checkedIds, temp);
} else { } else {
if (checkedIds.indexOf(item.id) !== -1) { if (checkedIds.indexOf(item.id) !== -1) {
temp.push(item.id); temp.push(item.id);
} }
} }
} }
return temp; return temp;
} }
function authzClose() { function authzClose() {
authzDialogVisible.value = false; authzDialogVisible.value = false;
} }
async function authzOpen(roleId: any) { async function authzOpen(roleId: any) {
permissionTreeData.value = await permissionApi.GetTree(); permissionTreeData.value = await permissionApi.GetTree();
checkedKeys.value = []; checkedKeys.value = [];
currentRoleId.value = roleId; currentRoleId.value = roleId;
// this.treeData = ret.data // this.treeData = ret.data
await updateChecked(roleId); await updateChecked(roleId);
authzDialogVisible.value = true; authzDialogVisible.value = true;
} }
async function updateChecked(roleId: any) { async function updateChecked(roleId: any) {
let checkedIds = await api.getPermissionIds(roleId); let checkedIds = await api.getPermissionIds(roleId);
// 找出所有的叶子节点 // 找出所有的叶子节点
checkedIds = getAllCheckedLeafNodeId(permissionTreeData.value, checkedIds, []); checkedIds = getAllCheckedLeafNodeId(permissionTreeData.value, checkedIds, []);
checkedKeys.value = checkedIds; checkedKeys.value = checkedIds;
} }
async function updatePermission() { async function updatePermission() {
const roleId = currentRoleId.value; const roleId = currentRoleId.value;
const { checked, halfChecked } = permissionTreeRef.value.getChecked(); const { checked, halfChecked } = permissionTreeRef.value.getChecked();
const allChecked = [...checked, ...halfChecked]; const allChecked = [...checked, ...halfChecked];
await api.DoAuthz(roleId, allChecked); await api.DoAuthz(roleId, allChecked);
authzClose(); authzClose();
//await updateChecked(roleId); //await updateChecked(roleId);
message.success("授权成功"); message.success("授权成功");
} }
return { return {
authzOpen, authzOpen,
updatePermission, updatePermission,
authzDialogVisible, authzDialogVisible,
permissionTreeData, permissionTreeData,
checkedKeys, checkedKeys,
permissionTreeRef, permissionTreeRef,
}; };
} }
export default defineComponent({ export default defineComponent({
name: "RoleManager", name: "RoleManager",
components: { FsPermissionTree }, components: { FsPermissionTree },
setup() { setup() {
//授权配置 //授权配置
const authz = useAuthz(); const { t } = useI18n();
const permission: UseCrudPermissionCompProps = { const authz = useAuthz();
prefix: "sys:auth:role", //权限代码前缀 const permission: UseCrudPermissionCompProps = {
extra: ({ hasActionPermission }: UseCrudPermissionExtraProps): any => { prefix: "sys:auth:role", //权限代码前缀
//额外按钮权限控制 extra: ({ hasActionPermission }: UseCrudPermissionExtraProps): any => {
return { rowHandle: { buttons: { authz: { show: hasActionPermission("authz") } } } }; //额外按钮权限控制
}, return { rowHandle: { buttons: { authz: { show: hasActionPermission("authz") } } } };
}; },
};
// 初始化crud配置 // 初始化crud配置
// 此处传入permission进行通用按钮权限设置,会通过commonOptions去设置actionbar和rowHandle的按钮的show属性 // 此处传入permission进行通用按钮权限设置,会通过commonOptions去设置actionbar和rowHandle的按钮的show属性
// 更多关于按钮权限的源代码设置,请参考 ./src/plugin/fast-crud/index.js 75-77行) // 更多关于按钮权限的源代码设置,请参考 ./src/plugin/fast-crud/index.js 75-77行)
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { authz, permission } }); const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { authz, permission } });
// 页面打开后获取列表数据 // 页面打开后获取列表数据
onMounted(() => { onMounted(() => {
crudExpose.doRefresh(); crudExpose.doRefresh();
}); });
onActivated(async () => { onActivated(async () => {
await crudExpose.doRefresh(); await crudExpose.doRefresh();
}); });
return { return {
crudBinding, crudBinding,
crudRef, crudRef,
...authz, ...authz,
}; t
}, };
},
}); });
</script> </script>
@@ -42,18 +42,18 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
fixed: "right", fixed: "right",
buttons: { buttons: {
unlock: { unlock: {
title: "解除登录锁定", title: t("certd.unlockLogin"),
text: null, text: null,
type: "link", type: "link",
icon: "ion:lock-open-outline", icon: "ion:lock-open-outline",
click: async ({ row }) => { click: async ({ row }) => {
Modal.confirm({ Modal.confirm({
title: "提示", title: t("certd.notice"),
content: "确定要解除该用户的登录锁定吗?", content: t("certd.confirmUnlock"),
onOk: async () => { onOk: async () => {
await api.Unlock(row.id); await api.Unlock(row.id);
notification.success({ notification.success({
message: "解除成功", message: t("certd.unlockSuccess"),
}); });
}, },
}); });
@@ -78,7 +78,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
}, },
}, },
createTime: { createTime: {
title: "创建时间", title: t("certd.createTime"),
type: "datetime", type: "datetime",
form: { show: false }, // 表单配置 form: { show: false }, // 表单配置
column: { column: {
@@ -96,13 +96,13 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
// } // }
// }, // },
username: { username: {
title: "用户名", title: t("certd.username"),
type: "text", type: "text",
search: { show: true }, // 开启查询 search: { show: true }, // 开启查询
form: { form: {
rules: [ rules: [
{ required: true, message: "请输入用户名" }, { required: true, message: t("certd.enterUsername") },
{ max: 50, message: "最大50个字符" }, { max: 50, message: t("certd.max50Chars") },
], ],
}, },
editForm: { component: { disabled: false } }, editForm: { component: { disabled: false } },
@@ -112,18 +112,18 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
}, },
}, },
password: { password: {
title: "密码", title: t("certd.password"),
type: "text", type: "text",
key: "password", key: "password",
column: { column: {
show: false, show: false,
}, },
form: { form: {
rules: [{ max: 50, message: "最大50个字符" }], rules: [{ max: 50, message: t("certd.max50Chars") }],
component: { component: {
showPassword: true, showPassword: true,
}, },
helper: "填写则修改密码", helper: t("certd.modifyPasswordIfFilled"),
}, },
}, },
nickName: { nickName: {
@@ -138,11 +138,11 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
}, },
}, },
email: { email: {
title: "邮箱", title: t("certd.emaila"),
type: "text", type: "text",
search: { show: true }, // 开启查询 search: { show: true }, // 开启查询
form: { form: {
rules: [{ max: 50, message: "最大50个字符" }], rules: [{ max: 50, message: t("certd.max50Chars") }],
}, },
column: { column: {
sorter: true, sorter: true,
@@ -150,11 +150,11 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
}, },
}, },
mobile: { mobile: {
title: "手机号", title: t("certd.mobile"),
type: "text", type: "text",
search: { show: true }, // 开启查询 search: { show: true }, // 开启查询
form: { form: {
rules: [{ max: 50, message: "最大50个字符" }], rules: [{ max: 50, message: t("certd.max50Chars") }],
}, },
column: { column: {
sorter: true, sorter: true,
@@ -162,12 +162,11 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
}, },
}, },
avatar: { avatar: {
title: "头像", title: t("certd.avatar"),
type: "cropper-uploader", type: "cropper-uploader",
column: { column: {
width: 70, width: 70,
component: { component: {
//设置高度,修复操作列错位的问题
style: { style: {
height: "30px", height: "30px",
width: "auto", width: "auto",
@@ -205,12 +204,12 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
}, },
}, },
status: { status: {
title: "状态", title: t("certd.status"),
type: "dict-switch", type: "dict-switch",
dict: dict({ dict: dict({
data: [ data: [
{ label: "启用", value: 1, color: "green" }, { label: t("certd.enabled"), value: 1, color: "green" },
{ label: "禁用", value: 0, color: "red" }, { label: t("certd.disabled"), value: 0, color: "red" },
], ],
}), }),
column: { column: {
@@ -220,7 +219,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
}, },
}, },
validTime: { validTime: {
title: "有效期", title: t("certd.validTime"),
type: "date", type: "date",
form: { form: {
show: userValidTimeEnabled, show: userValidTimeEnabled,
@@ -235,7 +234,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
return ""; return "";
} }
if (value < dayjs().valueOf()) { if (value < dayjs().valueOf()) {
return <a-tag color={"red"}></a-tag>; return <a-tag color={"red"}>{t("certd.expired")}</a-tag>;
} }
const date = dayjs(value).format("YYYY-MM-DD"); const date = dayjs(value).format("YYYY-MM-DD");
return ( return (
@@ -257,17 +256,17 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
}, },
}, },
remark: { remark: {
title: "备注", title: t("certd.remark"),
type: "text", type: "text",
column: { column: {
sorter: true, sorter: true,
}, },
form: { form: {
rules: [{ max: 100, message: "最大100个字符" }], rules: [{ max: 100, message: t("certd.max100Chars") }],
}, },
}, },
roles: { roles: {
title: "角色", title: t("certd.roles"),
type: "dict-select", type: "dict-select",
dict: dict({ dict: dict({
url: "/sys/authority/role/list", url: "/sys/authority/role/list",
@@ -8,244 +8,244 @@ import { useSettingStore } from "/@/store/settings";
import { Modal } from "ant-design-vue"; import { Modal } from "ant-design-vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => { const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query); return await api.GetList(query);
}; };
const editRequest = async ({ form, row }: EditReq) => { const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id; form.id = row.id;
const res = await api.UpdateObj(form); const res = await api.UpdateObj(form);
return res; return res;
}; };
const delRequest = async ({ row }: DelReq) => { const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id); return await api.DelObj(row.id);
}; };
const addRequest = async ({ form }: AddReq) => { const addRequest = async ({ form }: AddReq) => {
const res = await api.AddObj(form); const res = await api.AddObj(form);
return res; return res;
}; };
const userStore = useUserStore(); const userStore = useUserStore();
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]); const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys; context.selectedRowKeys = selectedRowKeys;
return { return {
crudOptions: { crudOptions: {
settings: { settings: {
plugins: { plugins: {
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并 //这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
rowSelection: { rowSelection: {
enabled: true, enabled: true,
order: -2, order: -2,
before: true, before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions, // handle: (pluginProps,useCrudProps)=>CrudOptions,
props: { props: {
multiple: true, multiple: true,
crossPage: true, crossPage: true,
selectedRowKeys selectedRowKeys
} }
} }
} }
}, },
request: { request: {
pageRequest, pageRequest,
addRequest, addRequest,
editRequest, editRequest,
delRequest delRequest
}, },
rowHandle: { rowHandle: {
minWidth: 200, minWidth: 200,
fixed: "right" fixed: "right"
}, },
columns: { columns: {
id: { id: {
title: "ID", title: "ID",
key: "id", key: "id",
type: "number", type: "number",
column: { column: {
width: 100 width: 100
}, },
form: { form: {
show: false show: false
} }
}, },
domain: { domain: {
title: "CNAME域名", title: t("certd.cnameDomain"),
type: "text", type: "text",
editForm: { editForm: {
component: { component: {
disabled: true disabled: true,
} },
}, },
search: { search: {
show: true show: true,
}, },
form: { form: {
component: { component: {
placeholder: "cname.handsfree.work" placeholder: t("certd.cnameDomainPlaceholder"),
}, },
helper: "需要一个右边DNS提供商注册的域名(也可以将其他域名的dns服务器转移到这几家来)。\nCNAME域名一旦确定不可修改,建议使用一级子域名", helper: t("certd.cnameDomainHelper"),
rules: [{ required: true, message: "此项必填" }] rules: [{ required: true, message: t("certd.requiredField") }],
}, },
column: { column: {
width: 200 width: 200,
} },
}, },
dnsProviderType: { dnsProviderType: {
title: "DNS提供商", title: t("certd.dnsProvider"),
type: "dict-select", type: "dict-select",
search: { search: {
show: true show: true,
}, },
dict: dict({ dict: dict({
url: "pi/dnsProvider/list", url: "pi/dnsProvider/list",
value: "key", value: "key",
label: "title" label: "title",
}), }),
form: { form: {
rules: [{ required: true, message: "此项必填" }] rules: [{ required: true, message: t("certd.requiredField") }],
}, },
column: { column: {
width: 150, width: 150,
component: { component: {
color: "auto" color: "auto",
} },
} },
}, },
accessId: { accessId: {
title: "DNS提供商授权", title: t("certd.dnsProviderAuthorization"),
type: "dict-select", type: "dict-select",
dict: dict({ dict: dict({
url: "/pi/access/list", url: "/pi/access/list",
value: "id", value: "id",
label: "name" label: "name",
}), }),
form: { form: {
component: { component: {
name: "access-selector", name: "access-selector",
vModel: "modelValue", vModel: "modelValue",
type: compute(({ form }) => { type: compute(({ form }) => {
return form.dnsProviderType; return form.dnsProviderType;
}) }),
}, },
rules: [{ required: true, message: "此项必填" }] rules: [{ required: true, message: t("certd.requiredField") }],
}, },
column: { column: {
width: 150, width: 150,
component: { component: {
color: "auto" color: "auto",
} },
} },
}, },
isDefault: { isDefault: {
title: "是否默认", title: t("certd.isDefault"),
type: "dict-switch", type: "dict-switch",
dict: dict({ dict: dict({
data: [ data: [
{ label: "是", value: true, color: "success" }, { label: t("certd.yes"), value: true, color: "success" },
{ label: "否", value: false, color: "default" } { label: t("certd.no"), value: false, color: "default" },
] ],
}), }),
form: { form: {
value: false, value: false,
rules: [{ required: true, message: "请选择是否默认" }] rules: [{ required: true, message: t("certd.selectIsDefault") }],
}, },
column: { column: {
align: "center", align: "center",
width: 100 width: 100,
} },
}, },
setDefault: { setDefault: {
title: "设置默认", title: t("certd.setDefault"),
type: "text", type: "text",
form: { form: {
show: false show: false,
}, },
column: { column: {
width: 100, width: 100,
align: "center", align: "center",
conditionalRenderDisabled: true, conditionalRenderDisabled: true,
cellRender: ({ row }) => { cellRender: ({ row }) => {
if (row.isDefault) { if (row.isDefault) {
return; return;
} }
const onClick = async () => { const onClick = async () => {
Modal.confirm({ Modal.confirm({
title: "提示", title: t("certd.prompt"),
content: `确定要设置为默认吗?`, content: t("certd.confirmSetDefault"),
onOk: async () => { onOk: async () => {
await api.SetDefault(row.id); await api.SetDefault(row.id);
await crudExpose.doRefresh(); await crudExpose.doRefresh();
} },
}); });
}; };
return ( return (
<a-button type={"link"} size={"small"} onClick={onClick}> <a-button type={"link"} size={"small"} onClick={onClick}>
{t("certd.setAsDefault")}
</a-button> </a-button>
); );
} },
} },
}, },
disabled: { disabled: {
title: "禁用/启用", title: t("certd.disabled"),
type: "dict-switch", type: "dict-switch",
dict: dict({ dict: dict({
data: [ data: [
{ label: "启用", value: false, color: "success" }, { label: t("certd.enabled"), value: false, color: "success" },
{ label: "禁用", value: true, color: "error" } { label: t("certd.disabledLabel"), value: true, color: "error" },
] ],
}), }),
form: { form: {
value: false value: false,
}, },
column: { column: {
width: 100, width: 100,
component: { component: {
title: "点击可禁用/启用", title: t("certd.clickToToggle"),
on: { on: {
async click({ value, row }) { async click({ value, row }) {
Modal.confirm({ Modal.confirm({
title: "提示", title: t("certd.prompt"),
content: `确定要${!value ? "禁用" : "启用"}吗?`, content: t("certd.confirmToggleStatus", { action: !value ? t("certd.disable") : t("certd.enable") }),
onOk: async () => { onOk: async () => {
await api.SetDisabled(row.id, !value); await api.SetDisabled(row.id, !value);
await crudExpose.doRefresh(); await crudExpose.doRefresh();
} },
}); });
} },
} },
} },
} },
}, },
createTime: { createTime: {
title: "创建时间", title: t("certd.createTime"),
type: "datetime", type: "datetime",
form: { form: {
show: false show: false,
}, },
column: { column: {
sorter: true, sorter: true,
width: 160, width: 160,
align: "center" align: "center",
} },
}, },
updateTime: { updateTime: {
title: "更新时间", title: t("certd.updateTime"),
type: "datetime", type: "datetime",
form: { form: {
show: false show: false,
}, },
column: { column: {
show: true, show: true,
width: 160 width: 160,
} },
} },
} }
} }
}; };
} }
@@ -1,60 +1,67 @@
<template> <template>
<fs-page class="page-cert"> <fs-page class="page-cert">
<template #header> <template #header>
<div class="title"> <div class="title">
CNAME服务配置 {{ t("certd.cnameTitle") }}
<span class="sub"> <span class="sub">
此处配置的域名作为其他域名校验的代理当别的域名需要申请证书时通过CNAME映射到此域名上来验证所有权好处是任何域名都可以通过此方式申请证书也无需填写AccessSecret {{ t("certd.cnameDescription") }}
<a href="https://certd.docmirror.cn/guide/feature/cname/" target="_blank">CNAME功能原理及使用说明</a> <a href="https://certd.docmirror.cn/guide/feature/cname/" target="_blank">
</span> {{ t("certd.cnameLinkText") }}
</div> </a>
</template> </span>
<fs-crud ref="crudRef" v-bind="crudBinding"> </div>
<template #pagination-left> </template>
<a-tooltip title="批量删除"> <fs-crud ref="crudRef" v-bind="crudBinding">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button> <template #pagination-left>
</a-tooltip> <a-tooltip :title="t('certd.batchDelete')">
</template> <fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</fs-crud> </a-tooltip>
</fs-page> </template>
</fs-crud>
</fs-page>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onActivated, onMounted } from "vue"; import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud"; import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud"; import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue"; import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api"; import { DeleteBatch } from "./api";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
defineOptions({ defineOptions({
name: "CnameProvider", name: "CnameProvider",
}); });
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions }); const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const selectedRowKeys = context.selectedRowKeys; const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => { const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) { if (selectedRowKeys.value?.length > 0) {
Modal.confirm({ Modal.confirm({
title: "确认", title: t("certd.confirmTitle"),
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`, content: t("certd.confirmDeleteBatch", { count: selectedRowKeys.value.length }),
async onOk() { async onOk() {
await DeleteBatch(selectedRowKeys.value); await DeleteBatch(selectedRowKeys.value);
message.info("删除成功"); message.info(t("certd.deleteSuccess"));
crudExpose.doRefresh(); crudExpose.doRefresh();
selectedRowKeys.value = []; selectedRowKeys.value = [];
}, },
}); });
} else { } else {
message.error("请先勾选记录"); message.error(t("certd.selectRecordsFirst"));
} }
}; };
// 页面打开后获取列表数据 // 页面打开后获取列表数据
onMounted(() => { onMounted(() => {
crudExpose.doRefresh(); crudExpose.doRefresh();
}); });
onActivated(async () => { onActivated(async () => {
await crudExpose.doRefresh(); await crudExpose.doRefresh();
}); });
</script> </script>
<style lang="less"></style> <style lang="less"></style>
File diff suppressed because it is too large Load Diff
@@ -1,58 +1,63 @@
<template> <template>
<fs-page class="page-cert"> <fs-page class="page-cert">
<template #header> <template #header>
<div class="title"> <div class="title">
插件管理 {{ t("certd.pluginManagement") }}
<span class="sub">自定义插件处于BETA测试版后续可能会有破坏性变更</span> <span class="sub">{{ t("certd.pluginBetaWarning") }}</span>
</div> </div>
</template> </template>
<fs-crud ref="crudRef" v-bind="crudBinding"> <fs-crud ref="crudRef" v-bind="crudBinding">
<!-- <template #pagination-left>--> <!-- <template #pagination-left>-->
<!-- <a-tooltip title="批量删除">--> <!-- <a-tooltip :title="t('certd.batchDelete')">-->
<!-- <fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>--> <!-- <fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>-->
<!-- </a-tooltip>--> <!-- </a-tooltip>-->
<!-- </template>--> <!-- </template>-->
</fs-crud> </fs-crud>
</fs-page> </fs-page>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onActivated, onMounted } from "vue"; import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud"; import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud"; import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue"; import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api"; import { DeleteBatch } from "./api";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
defineOptions({ defineOptions({
name: "SysPlugin", name: "SysPlugin",
}); });
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions }); const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
onActivated(async () => { onActivated(async () => {
await crudExpose.doRefresh(); await crudExpose.doRefresh();
}); });
const selectedRowKeys = context.selectedRowKeys; const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => { const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) { if (selectedRowKeys.value?.length > 0) {
Modal.confirm({ Modal.confirm({
title: "确认", title: t("certd.confirm"),
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`, content: t("certd.batchDeleteConfirm", { count: selectedRowKeys.value.length }),
async onOk() { async onOk() {
await DeleteBatch(selectedRowKeys.value); await DeleteBatch(selectedRowKeys.value);
message.info("删除成功"); message.info(t("certd.deleteSuccess"));
crudExpose.doRefresh(); crudExpose.doRefresh();
selectedRowKeys.value = []; selectedRowKeys.value = [];
}, },
}); });
} else { } else {
message.error("请先勾选记录"); message.error(t("certd.pleaseSelectRecord"));
} }
}; };
// 页面打开后获取列表数据 // 页面打开后获取列表数据
onMounted(() => { onMounted(() => {
crudExpose.doRefresh(); crudExpose.doRefresh();
}); });
</script> </script>
<style lang="less"></style> <style lang="less"></style>
@@ -1,73 +1,86 @@
<template> <template>
<fs-page class="page-setting-email"> <fs-page class="page-setting-email">
<template #header> <template #header>
<div class="title"> <div class="title">
邮件服务器设置 {{ t('certd.emailServerSettings') }}
<span class="sub">设置邮件发送服务器</span> <span class="sub">{{ t('certd.setEmailSendingServer') }}</span>
</div> </div>
</template> </template>
<div class="flex-o"> <div class="flex-o">
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" class="email-form-box" @finish="onFinish" @finish-failed="onFinishFailed"> <a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }"
<div v-if="!formState.usePlus" class="email-form"> autocomplete="off" class="email-form-box" @finish="onFinish" @finish-failed="onFinishFailed">
<a-form-item label="使用自定义邮件服务器"> </a-form-item> <div v-if="!formState.usePlus" class="email-form">
<a-form-item label="SMTP域名" name="host" :rules="[{ required: true, message: '请输入smtp域名或ip' }]"> <a-form-item :label="t('certd.useCustomEmailServer')"> </a-form-item>
<a-input v-model:value="formState.host" /> <a-form-item :label="t('certd.smtpDomain')" name="host"
</a-form-item> :rules="[{ required: true, message: t('certd.pleaseEnterSmtpDomain') }]">
<a-input v-model:value="formState.host" />
</a-form-item>
<a-form-item label="SMTP端口" name="port" :rules="[{ required: true, message: '请输入smtp端口号' }]"> <a-form-item :label="t('certd.smtpPort')" name="port"
<a-input v-model:value="formState.port" /> :rules="[{ required: true, message: t('certd.pleaseEnterSmtpPort') }]">
</a-form-item> <a-input v-model:value="formState.port" />
</a-form-item>
<a-form-item label="用户名" :name="['auth', 'user']" :rules="[{ required: true, message: '请输入用户名' }]"> <a-form-item :label="t('certd.username')" :name="['auth', 'user']"
<a-input v-model:value="formState.auth.user" /> :rules="[{ required: true, message: t('certd.pleaseEnterUsername') }]">
</a-form-item> <a-input v-model:value="formState.auth.user" />
<a-form-item label="密码" :name="['auth', 'pass']" :rules="[{ required: true, message: '请输入密码' }]"> </a-form-item>
<a-input-password v-model:value="formState.auth.pass" /> <a-form-item :label="t('certd.password')" :name="['auth', 'pass']"
<div class="helper">如果是qq邮箱需要到qq邮箱的设置里面申请授权码作为密码</div> :rules="[{ required: true, message: t('certd.pleaseEnterPassword') }]">
</a-form-item> <a-input-password v-model:value="formState.auth.pass" />
<a-form-item label="发件邮箱" name="sender" :rules="[{ required: true, message: '请输入发件邮箱' }]"> <div class="helper">{{ t('certd.qqEmailAuthCodeHelper') }}</div>
<a-input v-model:value="formState.sender" /> </a-form-item>
</a-form-item> <a-form-item :label="t('certd.senderEmail')" name="sender"
<a-form-item label="是否ssl" name="secure"> :rules="[{ required: true, message: t('certd.pleaseEnterSenderEmail') }]">
<a-switch v-model:checked="formState.secure" /> <a-input v-model:value="formState.sender" />
<div class="helper">ssl和非ssl的smtp端口是不一样的注意修改端口</div> </a-form-item>
</a-form-item> <a-form-item :label="t('certd.useSsl')" name="secure">
<a-form-item label="忽略证书校验" :name="['tls', 'rejectUnauthorized']"> <a-switch v-model:checked="formState.secure" />
<a-switch v-model:checked="formState.tls.rejectUnauthorized" /> <div class="helper">{{ t('certd.sslPortNote') }}</div>
</a-form-item> </a-form-item>
<a-form-item :label="t('certd.ignoreCertValidation')" :name="['tls', 'rejectUnauthorized']">
<a-switch v-model:checked="formState.tls.rejectUnauthorized" />
</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 type="primary" html-type="submit">{{ t('certd.save') }}</a-button>
</a-form-item> </a-form-item>
</div> </div>
<div class="email-form"> <div class="email-form">
<a-form-item label="使用官方邮件服务器" name="usePlus"> <a-form-item :label="t('certd.useOfficialEmailServer')" name="usePlus">
<div class="flex-o"> <div class="flex-o">
<a-switch v-model:checked="formState.usePlus" :disabled="!settingStore.isPlus" @change="onUsePlusChanged" /> <a-switch v-model:checked="formState.usePlus" :disabled="!settingStore.isPlus"
<vip-button class="ml-5" mode="button"></vip-button> @change="onUsePlusChanged" />
</div> <vip-button class="ml-5" mode="button"></vip-button>
<div class="helper">使用官方邮箱服务器直接发邮件免除繁琐的配置</div> </div>
</a-form-item> <div class="helper">{{ t('certd.useOfficialEmailServerHelper') }}</div>
</div> </a-form-item>
</a-form> </div>
</div> </a-form>
<div class="email-form"> </div>
<a-form :model="testFormState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onTestSend"> <div class="email-form">
<a-form-item label="测试收件邮箱" name="receiver" :rules="[{ required: true, message: '请输入测试收件邮箱' }]"> <a-form :model="testFormState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }"
<a-input v-model:value="testFormState.receiver" /> autocomplete="off" @finish="onTestSend">
<div class="helper">保存后再点击测试</div> <a-form-item :label="t('certd.testReceiverEmail')" name="receiver"
<div class="helper">发送失败<a href="https://certd.docmirror.cn/guide/use/email/" target="_blank">邮件配置帮助文档</a></div> :rules="[{ required: true, message: t('certd.pleaseEnterTestReceiverEmail') }]">
<div class="helper">您还可以试试使用官方邮件服务器</div> <a-input v-model:value="testFormState.receiver" />
</a-form-item> <div class="helper">{{ t('certd.saveBeforeTest') }}</div>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }"> <div class="helper">{{ t('certd.sendFailHelpDoc') }}<a
<a-button type="primary" :loading="testFormState.loading" html-type="submit">测试</a-button> href="https://certd.docmirror.cn/guide/use/email/" target="_blank">{{
</a-form-item> t('certd.emailConfigHelpDoc') }}</a></div>
</a-form> <div class="helper">{{ t('certd.tryOfficialEmailServer') }}</div>
</div> </a-form-item>
</fs-page> <a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" :loading="testFormState.loading" html-type="submit">{{ t('certd.test')
}}</a-button>
</a-form-item>
</a-form>
</div>
</fs-page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive } from "vue"; import { reactive } from "vue";
import * as api from "../api"; import * as api from "../api";
@@ -75,96 +88,102 @@ import * as emailApi from "./api.email";
import { notification } from "ant-design-vue"; import { notification } from "ant-design-vue";
import { useSettingStore } from "/src/store/settings"; import { useSettingStore } from "/src/store/settings";
import * as _ from "lodash-es"; import * as _ from "lodash-es";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
defineOptions({ defineOptions({
name: "EmailSetting", name: "EmailSetting",
}); });
interface FormState { interface FormState {
host: string; host: string;
port: number; port: number;
auth: { auth: {
user: string; user: string;
pass: string; pass: string;
}; };
secure: boolean; // use TLS secure: boolean; // use TLS
tls: { tls: {
// do not fail on invalid certs // do not fail on invalid certs
rejectUnauthorized?: boolean; rejectUnauthorized?: boolean;
}; };
sender: string; sender: string;
usePlus: boolean; usePlus: boolean;
} }
const formState = reactive<Partial<FormState>>({ const formState = reactive<Partial<FormState>>({
auth: { auth: {
user: "", user: "",
pass: "", pass: "",
}, },
tls: {}, tls: {},
usePlus: false, usePlus: false,
}); });
async function load() { async function load() {
const data: any = await api.EmailSettingsGet(); const data: any = await api.EmailSettingsGet();
_.merge(formState, data); _.merge(formState, data);
} }
load(); load();
const onFinish = async (form: any) => { const onFinish = async (form: any) => {
console.log("Success:", form); console.log("Success:", form);
await api.EmailSettingsSave(form); await api.EmailSettingsSave(form);
notification.success({ notification.success({
message: "保存成功", message: t("certd.saveSuccess"),
}); });
}; };
const onFinishFailed = (errorInfo: any) => { const onFinishFailed = (errorInfo: any) => {
// console.log("Failed:", errorInfo); // console.log("Failed:", errorInfo);
}; };
async function onUsePlusChanged() { async function onUsePlusChanged() {
await api.EmailSettingsSave(formState); await api.EmailSettingsSave(formState);
} }
interface TestFormState { interface TestFormState {
receiver: string; receiver: string;
loading: boolean; loading: boolean;
} }
const testFormState = reactive<TestFormState>({ const testFormState = reactive<TestFormState>({
receiver: "", receiver: "",
loading: false, loading: false,
}); });
async function onTestSend() { async function onTestSend() {
testFormState.loading = true; testFormState.loading = true;
try { try {
await emailApi.TestSend(testFormState.receiver); await emailApi.TestSend(testFormState.receiver);
notification.success({ notification.success({
message: "发送成功", message: t("certd.sendSuccess"),
}); });
} finally { } finally {
testFormState.loading = false; testFormState.loading = false;
} }
} }
const settingStore = useSettingStore(); const settingStore = useSettingStore();
</script> </script>
<style lang="less"> <style lang="less">
.page-setting-email { .page-setting-email {
.email-form-box { .email-form-box {
display: flex; display: flex;
} }
.email-form {
width: 500px;
margin: 20px;
}
.helper { .email-form {
padding: 1px; width: 500px;
margin: 0px; margin: 20px;
color: #999; }
font-size: 10px;
} .helper {
padding: 1px;
margin: 0px;
color: #999;
font-size: 10px;
}
} }
</style> </style>
@@ -1,54 +1,59 @@
<template> <template>
<div class="sys-settings-form sys-settings-base"> <div class="sys-settings-form sys-settings-base">
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finish-failed="onFinishFailed"> <a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off"
<a-form-item label="ICP备案号" :name="['public', 'icpNo']"> @finish="onFinish" @finish-failed="onFinishFailed">
<a-input v-model:value="formState.public.icpNo" placeholder="粤ICP备xxxxxxx号" /> <a-form-item :label="t('certd.icpRegistrationNumber')" :name="['public', 'icpNo']">
</a-form-item> <a-input v-model:value="formState.public.icpNo" :placeholder="t('certd.icpPlaceholder')" />
<a-form-item label="网安备案号" :name="['public', 'mpsNo']"> </a-form-item>
<a-input v-model:value="formState.public.mpsNo" placeholder="京公网安备xxxxxxx号" /> <a-form-item :label="t('certd.publicSecurityRegistrationNumber')" :name="['public', 'mpsNo']">
</a-form-item> <a-input v-model:value="formState.public.mpsNo" :placeholder="t('certd.publicSecurityPlaceholder')" />
</a-form-item>
<a-form-item label="开启小助手" :name="['public', 'aiChatEnabled']"> <a-form-item :label="t('certd.enableAssistant')" :name="['public', 'aiChatEnabled']">
<a-switch v-model:checked="formState.public.aiChatEnabled" /> <a-switch v-model:checked="formState.public.aiChatEnabled" />
</a-form-item> </a-form-item>
<a-form-item label="允许爬虫" :name="['public', 'robots']"> <a-form-item :label="t('certd.allowCrawlers')" :name="['public', 'robots']">
<a-switch v-model:checked="formState.public.robots" /> <a-switch v-model:checked="formState.public.robots" />
</a-form-item> </a-form-item>
<a-form-item label="HTTP代理" :name="['private', 'httpProxy']" :rules="urlRules"> <a-form-item :label="t('certd.httpProxy')" :name="['private', 'httpProxy']" :rules="urlRules">
<a-input v-model:value="formState.private.httpProxy" placeholder="http://192.168.1.2:18010/" /> <a-input v-model:value="formState.private.httpProxy" :placeholder="t('certd.httpProxyPlaceholder')" />
<div class="helper">当某些网站被墙时可以配置</div> <div class="helper">{{ t('certd.httpProxyHelper') }}</div>
</a-form-item> </a-form-item>
<a-form-item label="HTTPS代理" :name="['private', 'httpsProxy']" :rules="urlRules"> <a-form-item :label="t('certd.httpsProxy')" :name="['private', 'httpsProxy']" :rules="urlRules">
<div class="flex"> <div class="flex">
<a-input v-model:value="formState.private.httpsProxy" placeholder="http://192.168.1.2:18010/" /> <a-input v-model:value="formState.private.httpsProxy"
<a-button class="ml-5" type="primary" :loading="testProxyLoading" title="保存后,再点击测试" @click="testProxy">测试</a-button> :placeholder="t('certd.httpsProxyPlaceholder')" />
</div> <a-button class="ml-5" type="primary" :loading="testProxyLoading"
<div class="helper">一般这两个代理填一样的保存后再测试</div> :title="t('certd.saveThenTestTitle')" @click="testProxy">{{ t('certd.testButton') }}</a-button>
</a-form-item> </div>
<div class="helper">{{ t('certd.httpsProxyHelper') }}</div>
</a-form-item>
<a-form-item label="双栈网络" :name="['private', 'dnsResultOrder']"> <a-form-item :label="t('certd.dualStackNetwork')" :name="['private', 'dnsResultOrder']">
<a-select v-model:value="formState.private.dnsResultOrder"> <a-select v-model:value="formState.private.dnsResultOrder">
<a-select-option value="verbatim">默认</a-select-option> <a-select-option value="verbatim">{{ t('certd.default') }}</a-select-option>
<a-select-option value="ipv4first">IPV4优先</a-select-option> <a-select-option value="ipv4first">{{ t('certd.ipv4Priority') }}</a-select-option>
<a-select-option value="ipv6first">IPV6优先</a-select-option> <a-select-option value="ipv6first">{{ t('certd.ipv6Priority') }}</a-select-option>
</a-select> </a-select>
<div class="helper">如果选择IPv6优先需要在docker-compose.yaml中启用ipv6</div> <div class="helper">{{ t('certd.dualStackNetworkHelper') }}</div>
</a-form-item> </a-form-item>
<a-form-item label="启用公共CNAME服务" :name="['private', 'commonCnameEnabled']"> <a-form-item :label="t('certd.enableCommonCnameService')" :name="['private', 'commonCnameEnabled']">
<a-switch v-model:checked="formState.private.commonCnameEnabled" /> <a-switch v-model:checked="formState.private.commonCnameEnabled" />
<div class="helper">是否可以使用公共CNAME服务如果禁用且没有设置<router-link to="/sys/cname/provider">自定义CNAME服务</router-link>则无法使用CNAME代理方式申请证书</div> <div class="helper" v-html="t('certd.commonCnameHelper')"></div>
</a-form-item> </a-form-item>
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }"> <a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
<a-button :loading="saveLoading" type="primary" html-type="submit">保存</a-button> <a-button :loading="saveLoading" type="primary" html-type="submit">{{ t('certd.saveButton')
</a-form-item> }}</a-button>
</a-form> </a-form-item>
</div> </a-form>
</div>
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
import { SysSettings } from "/@/views/sys/settings/api"; import { SysSettings } from "/@/views/sys/settings/api";
@@ -57,90 +62,94 @@ import { merge } from "lodash-es";
import { useSettingStore } from "/@/store/settings"; import { useSettingStore } from "/@/store/settings";
import { notification } from "ant-design-vue"; import { notification } from "ant-design-vue";
import { util } from "/@/utils"; import { util } from "/@/utils";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
defineOptions({ defineOptions({
name: "SettingBase", name: "SettingBase",
}); });
const formState = reactive<Partial<SysSettings>>({ const formState = reactive<Partial<SysSettings>>({
public: { public: {
icpNo: "", icpNo: "",
mpsNo: "", mpsNo: "",
}, },
private: {}, private: {},
}); });
const urlRules = ref({ const urlRules = ref({
type: "url", type: "url",
message: "请输入正确的URL", message: "请输入正确的URL",
}); });
async function loadSysSettings() { async function loadSysSettings() {
const data: any = await api.SysSettingsGet(); const data: any = await api.SysSettingsGet();
merge(formState, data); merge(formState, data);
} }
const saveLoading = ref(false); const saveLoading = ref(false);
loadSysSettings(); loadSysSettings();
const settingsStore = useSettingStore(); const settingsStore = useSettingStore();
const onFinish = async (form: any) => { const onFinish = async (form: any) => {
try { try {
saveLoading.value = true; saveLoading.value = true;
await api.SysSettingsSave(form); await api.SysSettingsSave(form);
await settingsStore.loadSysSettings(); await settingsStore.loadSysSettings();
notification.success({ notification.success({
message: "保存成功", message: t('certd.saveSuccess'),
}); });
} finally { } finally {
saveLoading.value = false; saveLoading.value = false;
} }
}; };
const onFinishFailed = (errorInfo: any) => { const onFinishFailed = (errorInfo: any) => {
// console.log("Failed:", errorInfo); // console.log("Failed:", errorInfo);
}; };
async function stopOtherUserTimer() { async function stopOtherUserTimer() {
await api.stopOtherUserTimer(); await api.stopOtherUserTimer();
notification.success({ notification.success({
message: "停止成功", message: t('certd.stopSuccess'),
}); });
} }
const testProxyLoading = ref(false); const testProxyLoading = ref(false);
async function testProxy() { async function testProxy() {
testProxyLoading.value = true; testProxyLoading.value = true;
try { try {
const res = await api.TestProxy(); const res = await api.TestProxy();
let success = true; let success = true;
if (res.google !== true || res.baidu !== true) { if (res.google !== true || res.baidu !== true) {
success = false; success = false;
} }
const content = () => { const content = () => {
return ( return (
<div> <div>
<div>Google: {res.google === true ? "成功" : util.maxLength(res.google)}</div> <div>{t('certd.google')}: {res.google === true ? t('certd.success') : util.maxLength(res.google)}</div>
<div>Baidu: {res.baidu === true ? "成功" : util.maxLength(res.google)}</div> <div>{t('certd.baidu')}: {res.baidu === true ? t('certd.success') : util.maxLength(res.baidu)}</div>
</div> </div>
); );
}; };
if (!success) { if (!success) {
notification.error({ notification.error({
message: "测试失败", message: t('certd.testFailed'),
description: content, description: content,
}); });
return; return;
} }
notification.success({ notification.success({
message: "测试完成", message: t('certd.testCompleted'),
description: content, description: content,
}); });
} finally { } finally {
testProxyLoading.value = false; testProxyLoading.value = false;
} }
} }
</script> </script>
<style lang="less"> <style lang="less">
.sys-settings-base { .sys-settings-base {}
}
</style> </style>
@@ -1,67 +1,78 @@
<template> <template>
<div class="sys-settings-form sys-settings-register"> <div class="sys-settings-form sys-settings-register">
<a-form :model="formState" name="register" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish"> <a-form :model="formState" name="register" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }"
<a-form-item label="管理其他用户流水线" :name="['public', 'managerOtherUserPipeline']"> autocomplete="off" @finish="onFinish">
<a-switch v-model:checked="formState.public.managerOtherUserPipeline" /> <a-form-item :label="t('certd.manageOtherUserPipeline')" :name="['public', 'managerOtherUserPipeline']">
</a-form-item> <a-switch v-model:checked="formState.public.managerOtherUserPipeline" />
<a-form-item label="限制用户流水线数量" :name="['public', 'limitUserPipelineCount']"> </a-form-item>
<a-input-number v-model:value="formState.public.limitUserPipelineCount" /> <a-form-item :label="t('certd.limitUserPipelineCount')" :name="['public', 'limitUserPipelineCount']">
<div class="helper">0为不限制</div> <a-input-number v-model:value="formState.public.limitUserPipelineCount" />
</a-form-item> <div class="helper">{{ t('certd.limitUserPipelineCountHelper') }}</div>
<a-form-item label="开启自助注册" :name="['public', 'registerEnabled']"> </a-form-item>
<a-switch v-model:checked="formState.public.registerEnabled" /> <a-form-item :label="t('certd.enableSelfRegistration')" :name="['public', 'registerEnabled']">
</a-form-item> <a-switch v-model:checked="formState.public.registerEnabled" />
<a-form-item label="开启用户有效期" :name="['public', 'userValidTimeEnabled']"> </a-form-item>
<div class="flex-o"> <a-form-item :label="t('certd.enableUserValidityPeriod')" :name="['public', 'userValidTimeEnabled']">
<a-switch v-model:checked="formState.public.userValidTimeEnabled" :disabled="!settingsStore.isPlus" /> <div class="flex-o">
<vip-button class="ml-5" mode="button"></vip-button> <a-switch v-model:checked="formState.public.userValidTimeEnabled"
</div> :disabled="!settingsStore.isPlus" />
<div class="helper">有效期内用户可正常使用失效后流水线将被停用</div> <vip-button class="ml-5" mode="button"></vip-button>
</a-form-item> </div>
<template v-if="formState.public.registerEnabled"> <div class="helper">{{ t('certd.userValidityPeriodHelper') }}</div>
<a-form-item label="开启用户名注册" :name="['public', 'usernameRegisterEnabled']"> </a-form-item>
<a-switch v-model:checked="formState.public.usernameRegisterEnabled" /> <template v-if="formState.public.registerEnabled">
</a-form-item> <a-form-item :label="t('certd.enableUsernameRegistration')"
:name="['public', 'usernameRegisterEnabled']">
<a-switch v-model:checked="formState.public.usernameRegisterEnabled" />
</a-form-item>
<a-form-item label="开启邮箱注册" :name="['public', 'emailRegisterEnabled']"> <a-form-item :label="t('certd.enableEmailRegistration')" :name="['public', 'emailRegisterEnabled']">
<div class="flex-o"> <div class="flex-o">
<a-switch v-model:checked="formState.public.emailRegisterEnabled" :disabled="!settingsStore.isPlus" title="专业版功能" /> <a-switch v-model:checked="formState.public.emailRegisterEnabled"
<vip-button class="ml-5" mode="button"></vip-button> :disabled="!settingsStore.isPlus" :title="t('certd.proFeature')" />
</div> <vip-button class="ml-5" mode="button"></vip-button>
<div class="helper">需要<router-link to="/sys/settings/email">设置邮箱服务器</router-link></div> </div>
</a-form-item> <div class="helper">
<a-form-item label="开启手机号登录、注册" :name="['public', 'smsLoginEnabled']"> <router-link to="/sys/settings/email">{{ t('certd.emailServerSetup') }}</router-link>
<div class="flex-o"> </div>
<a-switch v-model:checked="formState.public.smsLoginEnabled" :disabled="!settingsStore.isComm" title="商业版功能" /> </a-form-item>
<vip-button class="ml-5" mode="comm"></vip-button> <a-form-item :label="t('certd.enableSmsLoginRegister')" :name="['public', 'smsLoginEnabled']">
</div> <div class="flex-o">
</a-form-item> <a-switch v-model:checked="formState.public.smsLoginEnabled" :disabled="!settingsStore.isComm"
<template v-if="formState.public.smsLoginEnabled"> :title="t('certd.commFeature')" />
<a-form-item label="短信提供商" :name="['private', 'sms', 'type']"> <vip-button class="ml-5" mode="comm"></vip-button>
<a-select v-model:value="formState.private.sms.type" @change="smsTypeChange"> </div>
<a-select-option value="aliyun">阿里云短信</a-select-option> </a-form-item>
<a-select-option value="yfysms">易发云短信</a-select-option> <template v-if="formState.public.smsLoginEnabled">
</a-select> <a-form-item :label="t('certd.smsProvider')" :name="['private', 'sms', 'type']">
</a-form-item> <a-select v-model:value="formState.private.sms.type" @change="smsTypeChange">
<template v-for="item of smsTypeDefineInputs" :key="item.simpleKey"> <a-select-option value="aliyun">{{ t('certd.aliyunSms') }}</a-select-option>
<fs-form-item v-model="formState.private.sms.config[item.simpleKey]" :path="'private.sms.config' + item.key" :item="item" /> <a-select-option value="yfysms">{{ t('certd.yfySms') }}</a-select-option>
</template> </a-select>
</a-form-item>
<template v-for="item of smsTypeDefineInputs" :key="item.simpleKey">
<fs-form-item v-model="formState.private.sms.config[item.simpleKey]"
:path="'private.sms.config' + item.key" :item="item" />
</template>
<a-form-item label="短信测试"> <a-form-item :label="t('certd.smsTest')">
<div class="flex"> <div class="flex">
<a-input v-model:value="testMobile" placeholder="输入测试手机号" /> <a-input v-model:value="testMobile" :placeholder="t('certd.testMobilePlaceholder')" />
<loading-button class="ml-5" title="保存后再点击测试" type="primary" :click="testSendSms">测试</loading-button> <loading-button class="ml-5" :title="t('certd.saveThenTest')" type="primary"
</div> :click="testSendSms">{{
<div class="helper">保存后再点击测试</div> t('certd.testButton') }}</loading-button>
</a-form-item> </div>
</template> <div class="helper">{{ t('certd.saveThenTest') }}</div>
</template> </a-form-item>
</template>
</template>
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 16 }"> <a-form-item label=" " :colon="false" :wrapper-col="{ span: 16 }">
<a-button :loading="saveLoading" type="primary" html-type="submit">保存</a-button> <a-button :loading="saveLoading" type="primary" html-type="submit">{{ t('certd.saveButton')
</a-form-item> }}</a-button>
</a-form> </a-form-item>
</div> </a-form>
</div>
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
@@ -71,123 +82,127 @@ import * as api from "/@/views/sys/settings/api";
import { merge } from "lodash-es"; import { merge } from "lodash-es";
import { useSettingStore } from "/@/store/settings"; import { useSettingStore } from "/@/store/settings";
import { notification } from "ant-design-vue"; import { notification } from "ant-design-vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
defineOptions({ defineOptions({
name: "SettingRegister", name: "SettingRegister",
}); });
const testMobile = ref(""); const testMobile = ref("");
async function testSendSms() { async function testSendSms() {
if (!testMobile.value) { if (!testMobile.value) {
notification.error({ notification.error({
message: "请输入测试手机号", message: t('certd.enterTestMobile'),
}); });
return; return;
} }
await api.TestSms({ await api.TestSms({
mobile: testMobile.value, mobile: testMobile.value,
}); });
notification.success({ notification.success({
message: "发送成功", message: t('certd.sendSuccess'),
}); });
} }
const formState = reactive<Partial<SysSettings>>({ const formState = reactive<Partial<SysSettings>>({
public: { public: {
registerEnabled: false, registerEnabled: false,
}, },
private: { private: {
sms: { sms: {
type: "aliyun", type: "aliyun",
config: {}, config: {},
}, },
}, },
}); });
const rules = { const rules = {
leastOneLogin: { leastOneLogin: {
validator: (rule: any, value: any) => { validator: (rule: any, value: any) => {
if (!formState.public.passwordLoginEnabled && !formState.public.smsLoginEnabled) { if (!formState.public.passwordLoginEnabled && !formState.public.smsLoginEnabled) {
return Promise.reject("密码登录和手机号登录至少开启一个"); return Promise.reject(t('certd.atLeastOneLoginRequired'));
} }
return Promise.resolve(); return Promise.resolve();
}, },
}, },
required: { required: {
required: true, required: true,
trigger: "change", trigger: "change",
message: "此项必填", message: t('certd.fieldRequired'),
}, },
}; };
async function smsTypeChange(value: string) {
if (formState.private?.sms?.config) {
formState.private.sms.config = {};
}
await loadTypeDefine(value); async function smsTypeChange(value: string) {
if (formState.private?.sms?.config) {
formState.private.sms.config = {};
}
await loadTypeDefine(value);
} }
const smsTypeDefineInputs: Ref = ref({}); const smsTypeDefineInputs: Ref = ref({});
async function loadTypeDefine(type: string) { async function loadTypeDefine(type: string) {
const define: any = await api.GetSmsTypeDefine(type); const define: any = await api.GetSmsTypeDefine(type);
const keys = Object.keys(define.input); const keys = Object.keys(define.input);
const inputs: any = {}; const inputs: any = {};
keys.forEach(key => { keys.forEach(key => {
const value = define.input[key]; const value = define.input[key];
value.simpleKey = key; value.simpleKey = key;
value.key = "private.sms.config." + key; value.key = "private.sms.config." + key;
if (!value.component) { if (!value.component) {
value.component = { value.component = {
name: "a-input", name: "a-input",
}; };
} }
if (!value.component.name) { if (!value.component.name) {
value.component.vModel = "value"; value.component.vModel = "value";
} }
if (!value.rules) { if (!value.rules) {
value.rules = []; value.rules = [];
} }
if (value.required) { if (value.required) {
value.rules.push(rules.required); value.rules.push(rules.required);
} }
inputs[key] = define.input[key]; inputs[key] = define.input[key];
}); });
smsTypeDefineInputs.value = inputs; smsTypeDefineInputs.value = inputs;
} }
async function loadSysSettings() { async function loadSysSettings() {
const data: any = await api.SysSettingsGet(); const data: any = await api.SysSettingsGet();
merge(formState, data); merge(formState, data);
if (data?.private.sms?.type) { if (data?.private.sms?.type) {
await loadTypeDefine(data.private.sms.type); await loadTypeDefine(data.private.sms.type);
} }
if (!settingsStore.isPlus) { if (!settingsStore.isPlus) {
formState.public.userValidTimeEnabled = false; formState.public.userValidTimeEnabled = false;
formState.public.emailRegisterEnabled = false; formState.public.emailRegisterEnabled = false;
} }
if (!settingsStore.isComm) { if (!settingsStore.isComm) {
formState.public.smsLoginEnabled = false; formState.public.smsLoginEnabled = false;
} }
} }
const saveLoading = ref(false); const saveLoading = ref(false);
loadSysSettings(); loadSysSettings();
const settingsStore = useSettingStore(); const settingsStore = useSettingStore();
const onFinish = async (form: any) => { const onFinish = async (form: any) => {
try { try {
saveLoading.value = true; saveLoading.value = true;
await api.SysSettingsSave(form); await api.SysSettingsSave(form);
await settingsStore.loadSysSettings(); await settingsStore.loadSysSettings();
notification.success({ notification.success({
message: "保存成功", message: t('certd.saveSuccess'),
}); });
} finally { } finally {
saveLoading.value = false; saveLoading.value = false;
} }
}; };
</script> </script>
<style lang="less"> <style lang="less">
.sys-settings-site { .sys-settings-site {}
}
</style> </style>
@@ -1,54 +1,63 @@
<template> <template>
<div class="sys-settings-form sys-settings-safe"> <div class="sys-settings-form sys-settings-safe">
<a-form ref="formRef" :model="formState" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off"> <a-form ref="formRef" :model="formState" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }"
<h2>站点隐藏</h2> autocomplete="off">
<a-form-item label="启用站点隐藏" :name="['hidden', 'enabled']" :required="true"> <h2>{{ t('certd.siteHide') }}</h2>
<div class="flex"> <a-form-item :label="t('certd.enableSiteHide')" :name="['hidden', 'enabled']" :required="true">
<a-switch v-model:checked="formState.hidden.enabled" /> <div class="flex">
</div> <a-switch v-model:checked="formState.hidden.enabled" />
</div>
<div class="helper"> <div class="helper">
可以在平时关闭站点的可访问性需要时再打开增强站点安全性 {{ t('certd.siteHideDescription') }}
<a href="https://certd.docmirror.cn/guide/feature/safe/hidden" class="flex items-center" target="_blank"> <a href="https://certd.docmirror.cn/guide/feature/safe/hidden" class="flex items-center"
<span>帮助说明</span> target="_blank">
<fs-icon class="ml-1" icon="mingcute:question-line"></fs-icon <span>{{ t('certd.helpDoc') }}</span>
></a> <fs-icon class="ml-1" icon="mingcute:question-line"></fs-icon></a>
</div> </div>
</a-form-item> </a-form-item>
<a-form-item v-if="formState.hidden.enabled" label="随机地址" :name="['hidden', 'openPath']" :required="true"> <a-form-item v-if="formState.hidden.enabled" :label="t('certd.randomAddress')"
<a-input-search v-model:value="formState.hidden.openPath" :allow-clear="true" @search="changeOpenPath"> :name="['hidden', 'openPath']" :required="true">
<template #enterButton> <a-input-search v-model:value="formState.hidden.openPath" :allow-clear="true" @search="changeOpenPath">
<fs-icon icon="ion:refresh"></fs-icon> <template #enterButton>
</template> <fs-icon icon="ion:refresh"></fs-icon>
</a-input-search> </template>
<div class="helper">站点被隐藏后需要访问此URL解锁才能正常访问</div> </a-input-search>
</a-form-item> <div class="helper">{{ t('certd.siteHideUrlHelper') }}</div>
<a-form-item v-if="formState.hidden.enabled" label="完整解除隐藏地址" :name="['hidden', 'openPath']" :required="true"> </a-form-item>
<div class="flex"><fs-copyable v-model="openUrl" class="flex-inline"></fs-copyable></div> <a-form-item v-if="formState.hidden.enabled" :label="t('certd.fullUnlockUrl')"
<div class="helper red">请保存好此地址</div> :name="['hidden', 'openPath']" :required="true">
</a-form-item> <div class="flex"><fs-copyable v-model="openUrl" class="flex-inline"></fs-copyable></div>
<a-form-item v-if="formState.hidden.enabled" label="解除密码" :name="['hidden', 'openPassword']" :required="false"> <div class="helper red">{{ t('certd.saveThisUrl') }}</div>
<a-input-password v-model:value="formState.hidden.openPassword" :allow-clear="true" /> </a-form-item>
<div class="helper">解除隐藏时需要输入密码第一次需要设置密码填写则重置密码</div> <a-form-item v-if="formState.hidden.enabled" :label="t('certd.unlockPassword')"
</a-form-item> :name="['hidden', 'openPassword']" :required="false">
<a-form-item v-if="formState.hidden.enabled" label="自动隐藏时间" :name="['hidden', 'autoHiddenTimes']" :required="true"> <a-input-password v-model:value="formState.hidden.openPassword" :allow-clear="true" />
<a-input-number v-model:value="formState.hidden.autoHiddenTimes" :allow-clear="true" /> <div class="helper">{{ t('certd.unlockPasswordHelper') }}</div>
<div class="helper">多少分钟内无请求自动隐藏</div> </a-form-item>
</a-form-item> <a-form-item v-if="formState.hidden.enabled" :label="t('certd.autoHideTime')"
<a-form-item v-if="formState.hidden.enabled" label="隐藏开放接口" :name="['hidden', 'hiddenOpenApi']" :required="true"> :name="['hidden', 'autoHiddenTimes']" :required="true">
<a-switch v-model:checked="formState.hidden.hiddenOpenApi" /> <a-input-number v-model:value="formState.hidden.autoHiddenTimes" :allow-clear="true" />
<div class="helper">是否隐藏开放接口是否放开/api/v1开头的接口</div> <div class="helper">{{ t('certd.autoHideTimeHelper') }}</div>
</a-form-item> </a-form-item>
<a-form-item v-if="formState.hidden.enabled" label="立即隐藏站点"> <a-form-item v-if="formState.hidden.enabled" :label="t('certd.hideOpenApi')"
<loading-button class="ml-1" type="primary" html-type="button" :click="doHiddenImmediate">立即隐藏</loading-button> :name="['hidden', 'hiddenOpenApi']" :required="true">
</a-form-item> <a-switch v-model:checked="formState.hidden.hiddenOpenApi" />
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 16 }"> <div class="helper">{{ t('certd.hideOpenApiHelper') }}</div>
<loading-button type="primary" html-type="button" :click="onClick">保存</loading-button> </a-form-item>
</a-form-item> <a-form-item v-if="formState.hidden.enabled" :label="t('certd.hideSiteImmediately')">
</a-form> <loading-button class="ml-1" type="primary" html-type="button" :click="doHiddenImmediate">{{
</div> t('certd.hideImmediately') }}</loading-button>
</a-form-item>
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 16 }">
<loading-button type="primary" html-type="button" :click="onClick">{{ t('certd.save')
}}</loading-button>
</a-form-item>
</a-form>
</div>
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import { computed, reactive, ref } from "vue"; import { computed, reactive, ref } from "vue";
import { merge } from "lodash-es"; import { merge } from "lodash-es";
@@ -56,106 +65,109 @@ import { Modal, notification } from "ant-design-vue";
import { request } from "/@/api/service"; import { request } from "/@/api/service";
import { util, utils } from "/@/utils"; import { util, utils } from "/@/utils";
import { useSettingStore } from "/@/store/settings"; import { useSettingStore } from "/@/store/settings";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
defineOptions({ defineOptions({
name: "SettingSafe", name: "SettingSafe",
}); });
const settingsStore = useSettingStore(); const settingsStore = useSettingStore();
const api = { const api = {
async SettingGet() { async SettingGet() {
return await request({ return await request({
url: "/sys/settings/safe/get", url: "/sys/settings/safe/get",
method: "post", method: "post",
}); });
}, },
async SettingSave(data: any) { async SettingSave(data: any) {
return await request({ return await request({
url: "/sys/settings/safe/save", url: "/sys/settings/safe/save",
method: "post", method: "post",
data, data,
}); });
}, },
async HiddenImmediate() { async HiddenImmediate() {
return await request({ return await request({
url: "/sys/settings/safe/hidden", url: "/sys/settings/safe/hidden",
method: "post", method: "post",
}); });
}, },
}; };
const defaultState = { const defaultState = {
hidden: { hidden: {
enabled: false, enabled: false,
autoHiddenTimes: 5, autoHiddenTimes: 5,
hiddenOpenApi: false, hiddenOpenApi: false,
}, },
}; };
const formRef = ref<any>(defaultState); const formRef = ref<any>(defaultState);
type SiteHidden = { type SiteHidden = {
enabled: boolean; enabled: boolean;
openPath?: string; openPath?: string;
autoHiddenTimes?: number; autoHiddenTimes?: number;
openPassword?: string; openPassword?: string;
hiddenOpenApi?: boolean; hiddenOpenApi?: boolean;
}; };
const formState = reactive< const formState = reactive<
Partial<{ Partial<{
hidden: SiteHidden; hidden: SiteHidden;
}> }>
>({ >({
hidden: { enabled: false }, hidden: { enabled: false },
}); });
function changeOpenPath() { function changeOpenPath() {
formState.hidden.openPath = util.randomString(16); formState.hidden.openPath = util.randomString(16);
} }
async function loadSettings() { async function loadSettings() {
const data: any = await api.SettingGet(); const data: any = await api.SettingGet();
merge(formState, defaultState, formState, data); merge(formState, defaultState, formState, data);
if (!formState.hidden.openPath) { if (!formState.hidden.openPath) {
changeOpenPath(); changeOpenPath();
} }
} }
loadSettings(); loadSettings();
const openUrl = computed(() => { const openUrl = computed(() => {
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.pathname = `/api/unhidden/${formState.hidden?.openPath || ""}`; url.pathname = `/api/unhidden/${formState.hidden?.openPath || ""}`;
//@ts-ignore //@ts-ignore
url.query = undefined; url.query = undefined;
url.hash = ""; url.hash = "";
return url.href; return url.href;
}); });
const onClick = async () => { const onClick = async () => {
const form = await formRef.value.validateFields(); const form = await formRef.value.validateFields();
//密码md5 //密码md5
// if (form.hidden?.openPassword) { // if (form.hidden?.openPassword) {
// form.hidden.openPassword = util.hash.md5(form.hidden.openPassword); // form.hidden.openPassword = util.hash.md5(form.hidden.openPassword);
// } // }
await api.SettingSave(form); await api.SettingSave(form);
await loadSettings(); await loadSettings();
notification.success({ notification.success({
message: "保存成功", message: t('certd.saveSuccess'),
}); });
}; };
async function doHiddenImmediate() { async function doHiddenImmediate() {
Modal.confirm({ Modal.confirm({
title: "确定要立即隐藏站点吗?", title: t('certd.confirmHideSiteTitle'),
content: "隐藏后,将无法访问站点,请谨慎操作", content: t('certd.confirmHideSiteContent'),
async onOk() { async onOk() {
await api.HiddenImmediate(); await api.HiddenImmediate();
notification.success({ notification.success({
message: "站点已隐藏", message: t('certd.siteHiddenSuccess'),
}); });
}, },
}); });
} }
</script> </script>
<style lang="less"> <style lang="less">
.sys-settings-base { .sys-settings-base {}
}
</style> </style>