add license check for desktop/mobile

This commit is contained in:
2025-10-21 15:19:43 +08:00
parent 2472f7bcf9
commit 6649680296
14 changed files with 1008 additions and 96 deletions

View File

@@ -214,9 +214,8 @@ desktop/mobile 的 js 会被 `vite` 使用 `lib` 模式打包,从而将所有
在 desktop/mobile直接使用
```ts
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
import client from '@/plugins/kintoneClient.ts'
const client = new KintoneRestAPIClient();
client.app...
```

View File

@@ -19,7 +19,10 @@
import type { Setting } from '@/types';
import { useI18n } from 'vue-i18n';
import { useKintoneClient } from '@/composables/useKintoneClient';
import { useLicenseManager } from '@/composables/useLicenseManager';
import type { Spinner } from 'kintone-ui-component';
import LicenseModal from '@/components/LicenseModal.vue';
import LicenseNotification from '@/components/LicenseNotification.vue';
import { nextTick, onMounted, reactive, ref, shallowRef, watch } from 'vue';
@@ -41,10 +44,21 @@ const loading = ref(false);
const mainArea = shallowRef<HTMLElement | null>(null);
const spinner = shallowRef<Spinner | null>(null);
const licenseManager = useLicenseManager();
onMounted(async () => {
// 等待页面完全渲染后再显示加载状态,实现更平滑的用户体验
nextTick(async () => {
loading.value = true;
// 检查许可证
if (!licenseManager.canAccessFeatures()) {
// 没有权限或许可证无效,显示弹框或错误提示
showLicenseError();
loading.value = false;
return;
}
// 获取已保存的插件配置
const savedSetting = kintone.plugin.app.getConfig(props.pluginId);
setting.buttonName = savedSetting?.buttonName || $t('config.button.default');
@@ -57,6 +71,9 @@ onMounted(async () => {
// 模拟加载时间,展示 spinner 效果
await new Promise((resolve) => setTimeout(resolve, 500));
loading.value = false;
// 检查许可证状态,显示相关信息
checkLicenseStatus();
});
});
@@ -98,6 +115,54 @@ async function save() {
});
}
/**
* 显示许可证错误
*/
function showLicenseError() {
// 添加许可证状态显示区域
const licenseStatusElement = document.createElement('div');
licenseStatusElement.className = 'license-status';
licenseStatusElement.innerHTML = `
<div class="license-error">
<h3>许可证无效</h3>
<p>您的插件许可证已过期或无效,请联系管理员或购买许可证。</p>
<button class="license-refresh-btn" onclick="window.location.reload()">重新检查</button>
</div>
`;
// 插入到页面顶部
const mainArea = document.getElementById('main-area');
if (mainArea && mainArea.parentNode) {
mainArea.parentNode.insertBefore(licenseStatusElement, mainArea);
}
}
/**
* 检查许可证状态并显示相关信息
*/
function checkLicenseStatus() {
const licenseInfo = licenseManager.getLicenseDisplayInfo();
if (licenseInfo) {
// 添加许可证状态显示
const statusElement = document.createElement('div');
statusElement.className = 'license-status-info';
statusElement.innerHTML = `
<div class="license-info">
<h4>许可证状态</h4>
<p>到期时间: ${licenseInfo.expiryDate}</p>
<p>状态: ${licenseInfo.isExpired ? '已过期' : licenseInfo.remainingDays === Infinity ? '永久' : `剩余 ${licenseInfo.remainingDays}`}</p>
</div>
`;
// 插入到设置区域上方
const mainArea = document.getElementById('main-area');
if (mainArea && mainArea.parentNode) {
mainArea.parentNode.insertBefore(statusElement, mainArea);
}
}
}
/**
* 取消操作并返回插件列表
* 重定向到插件管理页面

297
src/css/license.css Normal file
View File

@@ -0,0 +1,297 @@
/* 许可证弹框样式 */
.license-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.license-modal-content {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 400px;
max-width: 600px;
}
.license-modal-content .modal-header {
padding: 20px 24px 0 24px;
border-bottom: 1px solid #e5e5e5;
margin-bottom: 20px;
}
.license-modal-content .modal-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.license-modal-content .modal-body {
padding: 0 24px;
}
.license-modal-content .modal-footer {
padding: 20px 24px;
border-top: 1px solid #e5e5e5;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.license-modal-content .btn {
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
color: #666;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.license-modal-content .btn-primary {
background: #007acc;
color: white;
border-color: #007acc;
}
.license-modal-content .btn-primary:hover {
background: #005aa3;
border-color: #005aa3;
}
.license-modal-content .btn-secondary:hover {
background: #f5f5f5;
}
.license-modal-content .expiry-modal {
/* 样式已在content中定义 */
}
.license-modal-content .expiry-info .expiry-message {
margin: 0 0 16px 0;
color: #666;
line-height: 1.5;
}
.license-modal-content .info-item {
display: flex;
margin-bottom: 8px;
font-size: 14px;
}
.license-modal-content .info-item .label {
font-weight: 600;
color: #333;
min-width: 100px;
margin-right: 8px;
}
.license-modal-content .info-item .value {
color: #666;
}
.license-modal-content .purchase-link {
display: block;
font-size: 12px;
color: #999;
margin-top: 4px;
}
/* 许可证通知样式 */
.license-notification {
position: fixed;
right: 20px;
z-index: 9999;
min-width: 320px;
max-width: 450px;
}
.license-notification .notification-content {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid #e5e5e5;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.license-notification .notification-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px 8px 16px;
border-bottom: 1px solid #f0f0f0;
}
.license-notification .notification-title {
font-weight: 600;
font-size: 14px;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
.license-notification .icon-warning {
font-size: 16px;
}
.license-notification .close-btn {
background: none;
border: none;
font-size: 20px;
color: #999;
cursor: pointer;
padding: 0;
line-height: 1;
margin-left: 8px;
}
.license-notification .close-btn:hover {
color: #666;
}
.license-notification .notification-message {
padding: 12px 16px;
font-size: 14px;
line-height: 1.5;
color: #666;
}
.license-notification .notification-actions {
padding: 8px 16px 16px 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.license-notification .dont-show-again {
display: flex;
align-items: center;
font-size: 12px;
color: #999;
margin: 0;
}
.license-notification .dont-show-again input {
margin-right: 6px;
}
.license-notification .action-buttons {
display: flex;
gap: 8px;
}
.license-notification .btn {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
color: #666;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.license-notification .btn-primary {
background: #007acc;
color: white;
border-color: #007acc;
}
.license-notification .btn-primary:hover {
background: #005aa3;
border-color: #005aa3;
}
.license-notification .btn-secondary:hover {
background: #f5f5f5;
}
/* 许可证状态显示样式 */
.license-status-error,
.license-status {
margin-bottom: 20px;
padding: 16px;
border-radius: 6px;
border: 1px solid #e5e5e5;
}
.license-status-error {
background-color: #fff5f5;
border-color: #feb2b2;
color: #c53030;
}
.license-status .license-error {
text-align: center;
}
.license-status .license-error h3 {
margin: 0 0 8px 0;
color: #c53030;
font-size: 16px;
}
.license-status .license-error p {
margin: 0 0 12px 0;
line-height: 1.5;
}
.license-status .license-refresh-btn {
background: #e53e3e;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.license-status .license-refresh-btn:hover {
background: #c53030;
}
.license-status-info {
margin-bottom: 20px;
padding: 16px;
border-radius: 6px;
border: 1px solid #e5e5e5;
background-color: #f7fafc;
}
.license-status-info .license-info h4 {
margin: 0 0 12px 0;
color: #2d3748;
font-size: 16px;
font-weight: 600;
}
.license-status-info .license-info p {
margin: 0 0 8px 0;
font-size: 14px;
color: #4a5568;
line-height: 1.4;
}
.license-status-info .license-info p:last-child {
margin-bottom: 0;
}

View File

@@ -14,4 +14,32 @@ export default {
default: '',
},
},
license: {
expiry: {
title: 'License Expired - {domain} - {plugin}',
message: 'Your plugin license has expired. To continue using this feature, please purchase a new license.',
domain: 'Domain',
plugin: 'Plugin',
expiryDate: 'Expiry Date'
},
button: {
purchase: 'Purchase License',
checkLicense: 'Check Again',
close: 'Close'
},
notification: {
dontShowAgain: 'Don\'t show again'
},
permission: {
noAccess: 'You do not have permission to access this feature',
adminRequired: 'Administrator privileges required'
},
status: {
checking: 'Checking license...',
valid: 'License is valid',
expired: 'License has expired',
expiringSoon: 'License expires soon',
permanent: 'Permanent license'
}
},
};

View File

@@ -1,17 +1,45 @@
// 日语语言包配置
// 包含配置页面相关的翻译文本
export default {
hello: 'こんにちは',
hello: "こんにちは!",
config: {
title: 'kintone Vue テンプレート',
desc: 'kintoneプラグイン作成用 Vue テンプレート',
title: 'kintone Vue template',
desc: 'kintone Vue template for creating plugin',
button: {
label: 'ボタン名',
default: 'ボタン',
},
message: {
label: 'メッセージ',
default: '',
default: 'こんにちは、世界',
},
},
license: {
expiry: {
title: 'ライセンスの有効期限が切れました - {domain} - {plugin}',
message: 'プラグインのライセンス有効期限が切れています。この機能を引き続き使用するには、新しいライセンスを購入してください。',
domain: 'ドメイン',
plugin: 'プラグイン',
expiryDate: '有効期限'
},
button: {
purchase: 'ライセンスを購入',
checkLicense: '再確認',
close: '閉じる'
},
notification: {
dontShowAgain: '今後表示しない'
},
permission: {
noAccess: 'この機能にアクセスする権限がありません',
adminRequired: '管理者権限が必要です'
},
status: {
checking: 'ライセンスを確認しています...',
valid: 'ライセンスは有効です',
expired: 'ライセンスの有効期限が切れました',
expiringSoon: 'ライセンスの有効期限が近づいています',
permanent: '永久ライセンス'
}
},
};

View File

@@ -1,57 +1,67 @@
import i18n from '@/i18n';
import type { Setting } from '@/types';
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
import { LicenseService } from '@/services/licenseService';
import client from '@/plugins/kintoneClient.ts'
import { Button } from 'kintone-ui-component/lib/button';
(function (PLUGIN_ID) {
kintone.events.on('app.record.index.show', () => {
// 获取当前应用ID
const appIdNum = kintone.app.getId();
if (!appIdNum) {
return;
};
const appId = appIdNum.toString();
kintone.events.on('app.record.index.show', async () => {
// 授权了才能使用
LicenseService.loadPluginIfAuthorized(PLUGIN_ID,
async () => {
// 获取当前应用ID
const appIdNum = kintone.app.getId();
if (!appIdNum) {
return;
}
const appId = appIdNum.toString();
// 从插件配置中读取设置信息
const setting: Setting = kintone.plugin.app.getConfig(PLUGIN_ID);
// 从插件配置中读取设置信息
const setting: Setting = kintone.plugin.app.getConfig(PLUGIN_ID);
// 检查按钮是否已存在,防止翻页时重复添加
const btnId = 'template-btn-id';
if (document.getElementById(btnId)) {
return;
};
// 检查按钮是否已存在,防止翻页时重复添加
const btnId = 'template-btn-id';
if (document.getElementById(btnId)) {
return;
}
// 获取 Header 容器元素
const headerSpace = kintone.app.getHeaderMenuSpaceElement();
if (!headerSpace) {
throw new Error('このページではヘッダー要素が利用できません。');
}
// 获取 Header 容器元素
const headerSpace = kintone.app.getHeaderMenuSpaceElement();
if (!headerSpace) {
throw new Error('このページではヘッダー要素が利用できません。');
}
// 测试 i18n
const { t } = i18n.global;
// 测试 i18n
const { t } = i18n.global;
// 创建按钮
const button = new Button({
text: setting.buttonName,
type: 'submit',
id: btnId,
});
button.addEventListener('click', async () => {
try {
// 测试 KintoneRestAPIClient显示所有已启用的插件名
const client = new KintoneRestAPIClient();
const { plugins } = await client.app.getPlugins({
app: appId,
// 创建按钮
const button = new Button({
text: setting.buttonName,
type: 'submit',
id: btnId,
});
const pluginsInfo = plugins.map((p) => p.name).join('、');
button.addEventListener('click', async () => {
try {
// 测试 KintoneRestAPIClient显示所有已启用的插件名
const { plugins } = await client.app.getPlugins({
app: appId,
});
const pluginsInfo = plugins.map((p) => p.name).join('、');
const message = t('hello') + "\n" + setting.message + '\n--------\n【Plugins】 ' + pluginsInfo;
alert(message);
} catch (error) {
console.error('Failed to fetch plugins:', error);
}
});
headerSpace.appendChild(button);
const message = t('hello') + '\n' + setting.message + '\n--------\n【Plugins】 ' + pluginsInfo;
alert(message);
} catch (error) {
console.error('Failed to fetch plugins:', error);
}
});
headerSpace.appendChild(button);
},
{
expiryDialogTitle: '自定义插件到期提示',
expiryDialogMessage: '您的自定义插件许可证已过期,请联系管理员购买新的许可证。',
warningDialogTitle: '自定义插件即将到期',
warningDialogMessage: '您的自定义插件许可证还有3天就到期了请及时续费。',
},
);
});
})(kintone.$PLUGIN_ID);

View File

@@ -1,57 +1,67 @@
import i18n from '@/i18n';
import type { Setting } from '@/types';
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
import { LicenseService } from '@/services/licenseService';
import client from '@/plugins/kintoneClient.ts'
import { MobileButton } from 'kintone-ui-component/lib/mobile/button';
(function (PLUGIN_ID) {
kintone.events.on('mobile.app.record.index.show', () => {
// 获取当前应用ID
const appIdNum = kintone.mobile.app.getId();
if (!appIdNum) {
return;
};
const appId = appIdNum.toString();
// 授权了才能使用
LicenseService.loadPluginIfAuthorized(PLUGIN_ID,
async () => {
// 获取当前应用ID
const appIdNum = kintone.mobile.app.getId();
if (!appIdNum) {
return;
};
const appId = appIdNum.toString();
// 从插件配置中读取设置信息
const setting: Setting = kintone.plugin.app.getConfig(PLUGIN_ID);
// 从插件配置中读取设置信息
const setting: Setting = kintone.plugin.app.getConfig(PLUGIN_ID);
// 检查按钮是否已存在,防止翻页时重复添加
const btnId = 'template-btn-id';
if (document.getElementById(btnId)) {
return;
};
// 检查按钮是否已存在,防止翻页时重复添加
const btnId = 'template-btn-id';
if (document.getElementById(btnId)) {
return;
};
// 获取 Header 容器元素
const headerSpace = kintone.mobile.app.getHeaderSpaceElement();
if (!headerSpace) {
throw new Error('このページではヘッダー要素が利用できません。');
}
// 获取 Header 容器元素
const headerSpace = kintone.mobile.app.getHeaderSpaceElement();
if (!headerSpace) {
throw new Error('このページではヘッダー要素が利用できません。');
}
// 测试 i18n
const { t } = i18n.global;
// 测试 i18n
const { t } = i18n.global;
// 创建按钮
const button = new MobileButton({
text: setting.buttonName,
type: 'submit',
id: btnId,
});
button.addEventListener('click', async () => {
try {
// 测试 KintoneRestAPIClient显示所有已启用的插件名
const client = new KintoneRestAPIClient();
const { plugins } = await client.app.getPlugins({
app: appId,
});
const pluginsInfo = plugins.map((p) => p.name).join('、');
// 创建按钮
const button = new MobileButton({
text: setting.buttonName,
type: 'submit',
id: btnId,
});
button.addEventListener('click', async () => {
try {
// 测试 KintoneRestAPIClient显示所有已启用的插件名
const { plugins } = await client.app.getPlugins({
app: appId,
});
const pluginsInfo = plugins.map((p) => p.name).join('、');
const message = t('hello') + "\n" + setting.message + '\n--------\n【Plugins】 ' + pluginsInfo;
alert(message);
} catch (error) {
console.error('Failed to fetch plugins:', error);
}
});
headerSpace.appendChild(button);
const message = t('hello') + "\n" + setting.message + '\n--------\n【Plugins】 ' + pluginsInfo;
alert(message);
} catch (error) {
console.error('Failed to fetch plugins:', error);
}
});
headerSpace.appendChild(button);
},
{
expiryDialogTitle: '自定义插件到期提示',
expiryDialogMessage: '您的自定义插件许可证已过期,请联系管理员购买新的许可证。',
warningDialogTitle: '自定义插件即将到期',
warningDialogMessage: '您的自定义插件许可证还有3天就到期了请及时续费。',
},
);
});
})(kintone.$PLUGIN_ID);
})(kintone.$PLUGIN_ID);

View File

@@ -8,6 +8,7 @@
"js/desktop.js"
],
"css": [
"css/license.css",
"css/desktop.css"
]
},
@@ -19,6 +20,7 @@
],
"css": [
"css/51-modern-default.css",
"css/license.css",
"css/config.css"
],
"required_params": [

View File

@@ -1,16 +1,19 @@
import { type App } from 'vue';
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
// 创建客户端实例(单例)
const client = new KintoneRestAPIClient();
// 创建注入的 Symbol用于类型安全
export const KintoneClientInjectionKey = Symbol('kintone-client');
// 插件安装函数
export const kintoneClientPlugin = {
install(app: App) {
// 创建客户端实例(不需要参数)
const client = new KintoneRestAPIClient();
// 提供给整个应用使用
app.provide(KintoneClientInjectionKey, client);
},
};
// 导出客户端实例作为默认
export default client;

View File

@@ -0,0 +1,308 @@
import type { LicenseInfo, LicenseCheckResult } from '@/types/license';
import { LicenseStorage } from '@/utils/licenseStorage';
import { PermissionService } from '@/utils/permissions';
import { Notification } from 'kintone-ui-component/lib/notification';
import { createApp } from 'vue';
import i18n from '@/i18n';
export class LicenseService {
// 常量定义
private static readonly WARNING_DAYS_BEFORE_EXPIRY = 7;
private static PLUGIN_ID: string = '';
// ============ 基础工具函数 ============
/**
* 获取域名
*/
static getDomain(): string {
return window.location.hostname;
}
/**
* 获取插件ID
*/
static getPluginId(): string {
return this.PLUGIN_ID;
}
/**
* 检查许可证是否有效
* 已付费/当日获取是 true
*/
static checkLicenseAvailable(license: LicenseInfo) {
const domain = this.getDomain()
const pluginId = this.getPluginId()
// TODO jwt null
// 检查域名和插件ID是否与当前环境一致
if (license.domain !== domain || license.pluginId !== pluginId) {
return false;
}
// 检查是否付费
if (license.isPaid) {
return true;
}
const today = new Date();
// 检查存储是否过期(是否同一天)
const fetchDate = new Date(license.fetchTime).toDateString();
const todayStr = today.toDateString();
if (fetchDate !== todayStr) {
// 不是同一天,已过期
return false;
}
// 检查试用是否到期
const expiredTime = new Date(license.expiredTime);
if (expiredTime < today) {
return false;
}
return true
}
/**
* 许可证验证
*/
static async checkLicense(): Promise<LicenseCheckResult> {
const localLicense = this.getLocalLicenseInfo() || undefined
if (localLicense) {
return {
isLicenseValid: true,
license: localLicense,
};
}
return await this.checkLicenseRemote()
}
/**
* 远程许可证验证(模拟)
*/
static async checkLicenseRemote(): Promise<LicenseCheckResult> {
try {
// 这里应该是实际的API调用暂时模拟创建试用许可证
const response = await this.mockRemoteCheck(this.getDomain(), this.getPluginId());
const license = response.license!;
LicenseStorage.saveLicense(license);
return {
isLicenseValid: this.checkLicenseAvailable(license),
license,
isRemote: true,
};
} catch (error) {
console.error('Remote license check failed:', error);
return {
isLicenseValid: true,
};
}
}
// ============ 数据处理函数 ============
/**
* 检查是否快要到期
*/
static isExpiringSoon(expiryTimestamp: number): boolean {
const now = Date.now();
const warningTime = expiryTimestamp - (this.WARNING_DAYS_BEFORE_EXPIRY * 24 * 60 * 60 * 1000);
return now >= warningTime && expiryTimestamp > now;
}
/**
* 格式化到期时间
*/
static formatExpiryDate(timestamp: number): string {
if (timestamp === -1) return '永久';
return new Date(timestamp).toLocaleDateString('zh-CN');
}
/**
* 计算剩余天数
*/
static getDaysRemaining(timestamp: number): number {
if (timestamp === -1) return Infinity;
const now = new Date().getTime();
const remainingMs = timestamp - now;
return Math.ceil(remainingMs / (24 * 60 * 60 * 1000));
}
/**
* 获取许可证显示信息
*/
static getLicenseDisplayInfo(license: any) {
if (!license) return null;
return {
domain: this.getDomain(),
plugin: this.getPluginId(),
expiryDate: this.formatExpiryDate(license.expiredTime)
};
}
// ============ UI显示函数 ============
/**
* 显示到期弹框
*/
static showExpiryModal(licenseInfo: any, options?: {
expiryDialogTitle?: string;
expiryDialogMessage?: string;
}) {
return
}
/**
* 显示到期警告通知
*/
static showExpiryWarning(license: any, options?: {
warningDialogTitle?: string;
warningDialogMessage?: string;
}) {
const remainingDays = this.getDaysRemaining(license.expiredTime);
const defaultMessage = `您的插件许可证将在 ${remainingDays} 天后到期,请及时续期以避免服务中断。`;
// 使用自定义消息或默认消息
const message = options?.warningDialogMessage || defaultMessage;
// 检查是否已设置不再提醒
const dontShowKey = `license_notification_dont_show_again_${this.getPluginId()}`;
const dontShowAgain = localStorage.getItem(dontShowKey) === 'true';
if (dontShowAgain) return;
// 使用KUC Notification
const notification = new Notification({
text: message,
type: 'info',
duration: -1 // 不自动关闭
});
// 添加到页面
const container = document.body;
container.appendChild(notification);
// 监听关闭事件
notification.addEventListener('close', () => {
// 这里可以添加不再提醒的逻辑
});
}
// ============ 主要入口函数 ============
/**
* 检查插件功能访问权限并加载插件(如果获得授权)
*/
static async loadPluginIfAuthorized(
pluginId: string,
callback: () => void | Promise<void>,
options?: {
expiryDialogTitle?: string;
expiryDialogMessage?: string;
warningDialogTitle?: string;
warningDialogMessage?: string;
}
) {
this.PLUGIN_ID = pluginId;
try {
// 检查许可证(内部已经包含本地检查无效时自动获取远程的逻辑)
const licenseCheck = await this.checkLicense();
// 检查权限
const permissions = await PermissionService.checkPermissions();
const isManager = permissions.canManagePlugins;
// 许可证无效的情况
if (!licenseCheck.isLicenseValid) {
if (!isManager) {
// 普通用户静默输出
console.warn('License check failed, plugin functionality disabled');
} else {
// 管理员可以看到过期弹框
alert('许可证无效')
// this.showExpiryModal(this.getLicenseDisplayInfo(license.license), {
// expiryDialogTitle: options?.expiryDialogTitle,
// expiryDialogMessage: options?.expiryDialogMessage
// });
}
return;
}
// 许可证有效,如果快要到期,管理员可以看到警告
if (isManager && licenseCheck.license && !licenseCheck.license.isPaid && this.isExpiringSoon(licenseCheck.license.expiredTime)) {
// 管理员可以看到过期弹框
alert('即将过期')
// this.showExpiryWarning(licenseCheck.license, {
// warningDialogTitle: options?.warningDialogTitle,
// warningDialogMessage: options?.warningDialogMessage
// });
}
// 许可证有效,可以加载插件功能
await callback();
} catch (error) {
console.error('License check failed, plugin functionality disabled:', error);
}
}
// ============ 许可证信息管理 ============
/**
* 获取许可证信息(用于显示)
*/
static getLocalLicenseInfo(): LicenseInfo | null {
return LicenseStorage.getLicense(this.getPluginId());
}
/**
* 强制刷新许可证(清除本地缓存重新检查)
*/
static async forceRefreshLicense(): Promise<LicenseCheckResult> {
// 清除本地缓存
LicenseStorage.clearLicense(this.getPluginId());
return await this.checkLicenseRemote();
}
// ============ 模拟/测试函数 ============
/**
* 创建试用许可证
*/
private static mockCreateTrialLicense(): LicenseInfo {
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + 30);
return {
expiredTime: expiryDate.getTime(),
isPaid: false,
domain: this.getDomain(),
pluginId: this.getPluginId(),
fetchTime: new Date().getTime(),
};
}
/**
* 模拟远程验证生产环境中会被真实API替换
*/
private static async mockRemoteCheck(domain: string, pluginId: string): Promise<{ success: boolean; license?: LicenseInfo }> {
// 模拟API调用这里总是返回试用许可证
// 生产环境这里会调用后端API: POST /api/license/check
const license = this.mockCreateTrialLicense();
// 模拟有时创建失败的情况1%概率)
if (Math.random() < 0.01) {
return { success: false };
}
return {
success: true,
license,
};
}
}

View File

@@ -1,3 +1,4 @@
// 导出所有类型定义
// 主要导出应用设置相关的类型接口
export * from './model';
export * from './license';

45
src/types/license.d.ts vendored Normal file
View File

@@ -0,0 +1,45 @@
// 许可证相关类型定义
export interface LicenseInfo {
// 到期时间戳,后端需要是最后一天的 23:59:59 如果是永久许可证则为 -1
expiredTime: number;
// 是否是永久许可证
isPaid: boolean;
// 域名
domain: string;
// 插件ID
pluginId: string;
// 获取许可证的时间戳
fetchTime: number;
}
export interface LicenseCheckResult {
// 是否有效(许可证真正有效可用)
isLicenseValid: boolean;
// 许可证信息
license?: LicenseInfo;
// 是否来自远程检查(从 checkLicenseRemote 返回时为 true
isRemote?: boolean;
}
export interface NotificationConfig {
// 显示通知
show: boolean;
// 通知类型
type: 'expiry' | 'warning' | 'error';
// 通知标题
title: string;
// 通知内容
message: string;
// 是否有购买按钮
showPurchaseButton: boolean;
// 是否有检查按钮
showCheckButton: boolean;
// 不再提醒选项
showDontShowAgain: boolean;
}
export interface PermissionConfig {
// 是否可以管理插件(应用管理员权限)
canManagePlugins: boolean;
}

View File

@@ -0,0 +1,65 @@
// 本地存储加密工具类
import { LicenseService } from '@/services/licenseService';
import type { LicenseInfo } from '@/types/license';
export class LicenseStorage {
private static readonly STORAGE_KEY_PREFIX = 'alicorns_plugin_';
/**
* 生成存储key
*/
private static generateStorageKey(pluginId: string): string {
return `${this.STORAGE_KEY_PREFIX}${pluginId}`;
}
/**
* 保存许可证信息到本地存储
*/
static saveLicense(licenseInfo: LicenseInfo): void {
try {
// 直接存储LicenseInfo对象其中已包含fetchTime
const key = this.generateStorageKey(licenseInfo.pluginId);
localStorage.setItem(key, JSON.stringify(licenseInfo));
} catch (error) {
console.error('Failed to save license:', error);
throw error;
}
}
/**
* 从本地存储获取许可证信息
*/
static getLicense(pluginId: string): LicenseInfo | null {
try {
const key = this.generateStorageKey(pluginId);
const storedData = localStorage.getItem(key);
if (!storedData) return null;
const parsedData: LicenseInfo = JSON.parse(storedData);
const isValid = LicenseService.checkLicenseAvailable(parsedData)
if (!isValid) {
// 获取许可证信息失败,清理存储
this.clearLicense(pluginId);
return null;
}
return parsedData;
} catch (error) {
console.error('Failed to get license:', error);
// 如果获取失败,清空本地存储
this.clearLicense(pluginId);
return null;
}
}
/**
* 清除许可证信息
*/
static clearLicense(pluginId: string): void {
const key = this.generateStorageKey(pluginId);
localStorage.removeItem(key);
}
}

51
src/utils/permissions.ts Normal file
View File

@@ -0,0 +1,51 @@
// 权限检查工具类
import type { PermissionConfig } from '@/types/license';
import client from '@/plugins/kintoneClient.ts'
export class PermissionService {
/**
* 检查用户是否有管理插件的权限
* 基于kintone的权限系统判断用户是否可以管理应用插件
*/
static async checkPermissions(): Promise<PermissionConfig> {
try {
// 获取应用信息
const appId = kintone.app.getId();
if (!appId) {
return this.getDefaultPermissions();
}
// 检查应用的管理权限
const hasAdminRights = await this.checkAppAdminRights(appId);
return {
canManagePlugins: hasAdminRights,
};
} catch (error) {
console.error('Failed to check permissions:', error);
return this.getDefaultPermissions();
}
}
/**
* 检查应用管理员权限
*/
private static async checkAppAdminRights(appId: number): Promise<boolean> {
try {
await client.app.getAppAcl({ app: appId });
return true;
} catch (error) {
// 如果API调用失败默认认为没有权限
return false;
}
}
/**
* 获取默认权限配置(无权限)
*/
private static getDefaultPermissions(): PermissionConfig {
return {
canManagePlugins: false,
};
}
}