add license check for desktop/mobile
This commit is contained in:
@@ -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...
|
||||
```
|
||||
|
||||
|
||||
@@ -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
297
src/css/license.css
Normal 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;
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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: '永久ライセンス'
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -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": [
|
||||
|
||||
@@ -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;
|
||||
|
||||
308
src/services/licenseService.ts
Normal file
308
src/services/licenseService.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
1
src/types/index.d.ts
vendored
1
src/types/index.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
// 导出所有类型定义
|
||||
// 主要导出应用设置相关的类型接口
|
||||
export * from './model';
|
||||
export * from './license';
|
||||
|
||||
45
src/types/license.d.ts
vendored
Normal file
45
src/types/license.d.ts
vendored
Normal 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;
|
||||
}
|
||||
65
src/utils/licenseStorage.ts
Normal file
65
src/utils/licenseStorage.ts
Normal 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
51
src/utils/permissions.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user