This commit is contained in:
2025-10-23 18:03:02 +08:00
parent 8565b9513d
commit e531b50a0d
7 changed files with 174 additions and 184 deletions

View File

@@ -11,8 +11,8 @@
</div>
<div class="action-area">
<kuc-button text="キャンセル" type="normal" @click="cancel" />
<kuc-button text="保存する" class="save-btn" type="submit" @click="save" />
<kuc-button :text="$t('config.cancel')" type="normal" @click="cancel" />
<kuc-button :text="$t('config.save')" class="save-btn" type="submit" @click="save" />
</div>
<!-- 必须使用 v-show spinner dom 创建的时候不显示 -->
@@ -54,11 +54,14 @@ onMounted(async () => {
const savedSetting = kintone.plugin.app.getConfig(props.pluginId);
setting.buttonName = savedSetting?.buttonName || $t('config.button.default');
setting.message = savedSetting?.message || $t('config.message.default');
// 测试 KintoneRestAPIClient
const { plugins } = await client.app.getPlugins({
app: kintone.app.getId() as number,
});
const pluginsInfo = plugins.map((p: any) => p.name).join(';show');
const pluginsInfo = plugins.map((p: any) => p.name).join('');
console.log('pluginsInfo', pluginsInfo);
// 模拟加载时间,展示 spinner 效果
await new Promise((resolve) => setTimeout(resolve, 500));
@@ -79,7 +82,7 @@ watch(loading, (load) => {
*/
function validate(setting: Setting): boolean {
if (!setting.buttonName.trim()) {
error.value = 'ボタン名を入力してください。';
error.value = $t('config.button.error.required');
return false;
}
return true;

View File

@@ -2,17 +2,17 @@
<div v-if="shown" class="license-status-info">
<div class="license-content">
<div class="license-status-text">
<strong>许可证状态: </strong>
<strong>{{ $t('license.status.label') }}</strong>
<span v-if="licenseDisplayInfo" v-html="licenseStatusText"></span><span v-else> ... </span>
</div>
<div class="license-actions">
<template v-if="!licenseDisplayInfo.isPaid">
<button class="action-btn" @click="refreshLicenseStatus" :disabled="checking" ref="checkButton">检查</button>
<kuc-tooltip title="重新检查许可证状态" :container="checkButton"></kuc-tooltip>
<button class="action-btn main" @click="purchaseLicense" ref="buyButton">购买</button>
<kuc-tooltip title="购买后当前域名下所有应用都可使用" :container="buyButton"></kuc-tooltip>
<button class="action-btn" @click="refreshLicenseStatus" :disabled="checking" ref="checkButton">{{ $t('license.button.checkLicense.label') }}</button>
<kuc-tooltip :title="$t('license.button.checkLicense.desc')" :container="checkButton"></kuc-tooltip>
<button class="action-btn main" @click="purchaseLicense" ref="buyButton">{{ $t('license.button.purchase.label') }}</button>
<kuc-tooltip :title="$t('license.button.purchase.desc')" :container="buyButton"></kuc-tooltip>
</template>
<button v-else class="action-btn" @click="hidePaidMsg">不再显示</button>
<button v-else class="action-btn" @click="hidePaidMsg">{{ $t('license.button.hide') }}</button>
</div>
</div>
</div>
@@ -20,10 +20,13 @@
<script setup lang="ts">
import { ref, onMounted, computed, shallowRef } from 'vue';
import { useI18n } from 'vue-i18n';
import { LicenseService } from '@/services/LicenseService';
import type { LicenseInfo, LicenseCheckResult } from '@/types/license';
import type { LicenseInfo } from '@/types/license';
import { LicenseStorage } from '@/utils/LicenseStorage';
const { t: $t } = useI18n();// 配置国际化
type LicenseDisplayInfo = {
isPaid: boolean;
expiryDate: string;
@@ -44,7 +47,7 @@ const licenseDisplayInfo = computed<LicenseDisplayInfo>(() => {
if (!licenseInfo.value || loading.value) {
return {
isPaid: false,
expiryDate: '未知',
expiryDate: $t('license.status.unknown'),
isExpired: false,
remainingDays: 1,
};
@@ -74,7 +77,7 @@ const addPaidLabel = () => {
const isExist = !!document.querySelector('#license-label')
if (!shown.value && !isExist) {
const spanElement = document.createElement('span');
spanElement.textContent = '已授权';
spanElement.textContent = $t('license.status.permanent'),
spanElement.id = 'license-label';
document.querySelector('#app > .settings-heading')?.appendChild(spanElement);
}
@@ -93,29 +96,28 @@ const isPaidAreaShown = () => {
const licenseStatusText = computed(() => {
if (!licenseDisplayInfo.value) {
return '未知';
return $t('license.status.unknown');
}
if (loading.value) {
return '加载中...';
return $t('license.status.checking');
}
if (licenseDisplayInfo.value.isPaid) {
return '<span class="text-green">永久可用</span>';
return `<span class="text-green">${$t('license.status.permanentDisplay')}</span>`;
}
let status = `到期时间 ${licenseDisplayInfo.value.expiryDate} `;
let status = $t('license.expiry.expiryDate', { date: licenseDisplayInfo.value.expiryDate });
if (licenseDisplayInfo.value.isExpired) {
status += '(<span class="text-red">已过期</span>)';
} else if (licenseDisplayInfo.value.remainingDays == 0) {
status += '(今天)';
} else if (licenseDisplayInfo.value.remainingDays == 1) {
status += '(明天)';
} else {
status += '(剩余 ' + licenseDisplayInfo.value.remainingDays + ' 天)';
status += `(<span class="text-red">${$t('license.status.expired')}</span>)`;
return status;
}
const remainingDays = licenseDisplayInfo.value.remainingDays;
const days = $t('license.notification.days', remainingDays)
status += `(${days})`;
return status;
});

View File

@@ -6,40 +6,55 @@ export default {
title: 'kintone Vue template',
desc: 'kintone Vue template for creating plugin',
button: {
label: 'button name',
default: 'button',
label: 'Button name',
default: 'Button',
error: {
required: '@:config.button.label is required.'
}
},
message: {
label: 'message',
default: '',
},
cancel: 'Cancel',
save: 'Save',
},
error: {
noAreaError: 'Header menu space element is required.',
},
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'
expiryDate: 'Expiry Date {date}'
},
button: {
purchase: 'Purchase License',
checkLicense: 'Check Again',
close: 'Close'
purchase: {
label: 'Purchase',
desc: 'All applications under this domain can be used.'
},
checkLicense: {
label: 'Check',
desc: 'Recheck the license status.'
},
hide: 'Hide'
},
notification: {
dontShowAgain: 'Don\'t show again'
},
permission: {
noAccess: 'You do not have permission to access this feature',
adminRequired: 'Administrator privileges required'
days: 'today | tomorrow | {count} days later',
gotoLink: 'Please visit <a class="notification-link" href="{link}">Plugin Setting Page</a>.',
expired: 'Your plugin {plugin} license has expired.<br>@:license.notification.gotoLink',
warning: 'Your plugin {plugin} license will be expired {days}.<br>@:license.notification.gotoLink'
},
status: {
label: 'License Status: ',
checking: 'Checking license...',
valid: 'License is valid',
expired: 'License has expired',
expiringSoon: 'License expires soon',
permanent: 'Permanent license'
expired: 'Expired',
expiredDisplay: 'License has expired',
permanent: 'Permanent license',
permanentDisplay: 'Permanent license',
unknown: 'Unknown'
},
error: {
fetchFailed: 'Remote license check failed: {e}',
checkFailed: 'License check failed, plugin disabled: {e}'
}
},
};

View File

@@ -1,4 +1,7 @@
// 日语语言包配置
import { warn } from "vue";
// 包含配置页面相关的翻译文本
export default {
hello: "こんにちは!",
@@ -8,38 +11,53 @@ export default {
button: {
label: 'ボタン名',
default: 'ボタン',
error: {
required: '@:config.button.labelを入力してください。'
}
},
message: {
label: 'メッセージ',
default: 'こんにちは、世界',
default: '',
},
cancel: 'キャンセル',
save: '保存する',
},
error: {
noAreaError: 'このページではヘッダー要素が利用できません。',
},
license: {
expiry: {
title: 'ライセンスの有効期限が切れました - {domain} - {plugin}',
message: 'プラグインのライセンス有効期限が切れています。この機能を引き続き使用するには、新しいライセンスを購入してください。',
domain: 'ドメイン',
plugin: 'プラグイン',
expiryDate: '有効期限'
expiryDate: '有効期限 {date} ',
},
button: {
purchase: 'ライセンスを購入',
checkLicense: '再確認',
close: '閉じる'
purchase: {
label: '購入',
desc: '購入後、このドメイン下の全アプリで利用可能'
},
checkLicense: {
label: '再確認',
desc: 'ライセンス状態を再確認'
},
hide: '非表示'
},
notification: {
dontShowAgain: '今後表示しない'
},
permission: {
noAccess: 'この機能にアクセスする権限がありません',
adminRequired: '管理者権限が必要です'
days: '今日 | 明日 | {count}日後',
gotoLink: 'ご利用には<a class="notification-link" href="{link}">「プラグインの設定」</a>をご確認ください。',
expired: 'プラグイン「{plugin}」の有効期限が切れました。<br>@:license.notification.gotoLink',
warning: 'プラグイン「{plugin}」の試用期間が{days}で終了します。<br>@:license.notification.gotoLink'
},
status: {
checking: 'ライセンスを確認しています...',
valid: 'ライセンスは有効です',
expired: 'ライセンスの有効期限切れました',
expiringSoon: 'ライセンスの有効期限が近づいています',
permanent: '永久ライセンス'
label: 'ライセンス状態: ',
checking: '確認しています...',
expired: '期限切れ',
expiredDisplay: 'ライセンスの有効期限が切れました',
permanent: '永久ライセンス',
permanentDisplay: '永久利用可能',
unknown: '不明'
},
error: {
fetchFailed: 'リモートライセンスチェックに失敗しました: {e}',
checkFailed: 'ライセンスチェックに失敗しました。プラグイン機能が無効になりました: {e}'
}
},
};

View File

@@ -25,15 +25,15 @@ import { Button } from 'kintone-ui-component/lib/button';
return;
}
// 测试 i18n
const { t } = i18n.global;
// 获取 Header 容器元素
const headerSpace = kintone.app.getHeaderMenuSpaceElement();
if (!headerSpace) {
throw new Error('このページではヘッダー要素が利用できません。');
throw new Error(t('error.noAreaError'));
}
// 测试 i18n
const { t } = i18n.global;
// 创建按钮
const button = new Button({
text: setting.buttonName,
@@ -56,11 +56,6 @@ import { Button } from 'kintone-ui-component/lib/button';
});
headerSpace.appendChild(button);
},
{
expiryDialogTitle: '自定义插件到期提示',
expiryDialogMessage: '您的自定义插件许可证已过期,请联系管理员购买新的许可证。',
// warningDialogMessage: '您的自定义插件许可证还有3天就到期了请及时续费。',
},
);
});
})(kintone.$PLUGIN_ID);

View File

@@ -6,60 +6,55 @@ import { MobileButton } from 'kintone-ui-component/lib/mobile/button';
(function (PLUGIN_ID) {
kintone.events.on('mobile.app.record.index.show', () => {
// 授权了才能使用
LicenseService.loadPluginIfAuthorized(PLUGIN_ID,
async () => {
// 获取当前应用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('このページではヘッダー要素が利用できません。');
// 测试 i18n
const { t } = i18n.global;
// 获取 Header 容器元素
const headerSpace = kintone.mobile.app.getHeaderSpaceElement();
if (!headerSpace) {
throw new Error(t('error.noAreaError'));
}
// 创建按钮
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);
}
// 测试 i18n
const { t } = i18n.global;
// 创建按钮
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);
},
{
expiryDialogTitle: '自定义插件到期提示',
expiryDialogMessage: '您的自定义插件许可证已过期,请联系管理员购买新的许可证。',
// warningDialogMessage: '您的自定义插件许可证还有3天就到期了请及时续费。',
});
headerSpace.appendChild(button);
},
);
});

View File

@@ -1,9 +1,12 @@
import type { LicenseInfo, LicenseCheckResult } from '@/types/license';
import i18n from '@/i18n';
import { LicenseStorage } from '@/utils/LicenseStorage';
import { PermissionService } from '@/utils/permissions';
import { Notification } from 'kintone-ui-component/lib/notification';
import { MobileNotification } from 'kintone-ui-component/lib/mobile/notification';
const { t: $t } = i18n.global;
export class LicenseService {
// 常量定义
private static readonly WARNING_DAYS_BEFORE_EXPIRY = 7;
@@ -98,7 +101,7 @@ export class LicenseService {
};
} catch (error) {
console.error('Remote license check failed:', error);
console.error($t('license.error.fetchFailed', { e: error }));
return {
isLicenseValid: true,
};
@@ -121,7 +124,7 @@ export class LicenseService {
* 格式化到期时间
*/
static formatExpiryDate(timestamp: number): string {
if (timestamp === -1) return '永久';
if (timestamp === -1) return $t('license.status.permanent');
return new Date(timestamp).toLocaleDateString(kintone.getLoginUser().language);
}
@@ -135,56 +138,26 @@ export class LicenseService {
return Math.floor(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 async showExpiryWarning(license: any, options?: {
warningDialogMessage?: string;
}) {
const remainingDays = this.getDaysRemaining(license.expiredTime);
if (remainingDays <= 0) {
return
}
const msg = remainingDays > 1 ? ` ${remainingDays} 天后` : (remainingDays > 0 ? '明天' : '今天')
static async showNotification(license: LicenseInfo | undefined, isWarning: boolean) {
let message;
const link = `https://alicorn.cybozu.com/k/admin/app/${kintone.app.getId()}/plugin/config?pluginId=${this.getPluginId()}`
const defaultMessage = `您的插件将在${msg}试用结束<br>如需使用请进入<a class="notification-link" href="${link}">「プラグインの設定」</a>查看详情。`;
// 使用自定义消息或默认消息
const message = options?.warningDialogMessage || defaultMessage;
// 检查是否已设置不再提醒
// const settings = LicenseStorage.getSettings(this.getPluginId());
// if (settings.suppressMsgTime && this.isToday(settings.suppressMsgTime)) {
// return
// }
// delete settings.suppressMsgTime
// LicenseStorage.saveSetting(settings, this.getPluginId());
if (isWarning) {
// 尚未到期
const remainingDays = this.getDaysRemaining(license!.expiredTime);
if (remainingDays < 0) {
return
}
const days = $t('license.notification.days', remainingDays)
message = $t('license.notification.warning', { plugin: this.getPluginId(), link, days });
} else {
// 既に期限切れ
message = $t('license.notification.expired', { plugin: this.getPluginId(), link });
}
if (await kintone.isMobilePage()) {
const notification = new MobileNotification({
content: message,
@@ -193,7 +166,7 @@ export class LicenseService {
} else {
const notification = new Notification({
content: message,
type: 'info',
type: isWarning ? 'info' : 'danger',
});
notification.open();
}
@@ -207,11 +180,6 @@ export class LicenseService {
static async loadPluginIfAuthorized(
pluginId: string,
callback: () => void | Promise<void>,
options?: {
expiryDialogTitle?: string;
expiryDialogMessage?: string;
warningDialogMessage?: string;
}
) {
this.PLUGIN_ID = pluginId;
try {
@@ -222,18 +190,14 @@ export class LicenseService {
const permissions = await PermissionService.checkPermissions();
const isManager = permissions.canManagePlugins;
// 许可证无效的情况
// 许可证无效的情况,直接返回不加载
if (!licenseCheck.isLicenseValid) {
if (!isManager) {
// 普通用户静默输出
console.warn('License check failed, plugin functionality disabled');
console.warn($t('license.status.expiredDisplay'));
} else {
// 管理员可以看到过期弹框
alert('许可证无效')
// this.showExpiryModal(this.getLicenseDisplayInfo(license.license), {
// expiryDialogTitle: options?.expiryDialogTitle,
// expiryDialogMessage: options?.expiryDialogMessage
// });
this.showNotification(licenseCheck.license, false);
}
return;
}
@@ -244,16 +208,14 @@ export class LicenseService {
!licenseCheck.license.isPaid &&
this.isExpiringSoon(licenseCheck.license.expiredTime)) {
// 管理员可以看到过期弹框
this.showExpiryWarning(licenseCheck.license, {
warningDialogMessage: options?.warningDialogMessage
});
this.showNotification(licenseCheck.license, true);
}
// 许可证有效,可以加载插件功能
await callback();
} catch (error) {
console.error('License check failed, plugin functionality disabled:', error);
console.warn($t('license.error.checkFailed'));
}
}