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

View File

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

View File

@@ -6,40 +6,55 @@ export default {
title: 'kintone Vue template', title: 'kintone Vue template',
desc: 'kintone Vue template for creating plugin', desc: 'kintone Vue template for creating plugin',
button: { button: {
label: 'button name', label: 'Button name',
default: 'button', default: 'Button',
error: {
required: '@:config.button.label is required.'
}
}, },
message: { message: {
label: 'message', label: 'message',
default: '', default: '',
}, },
cancel: 'Cancel',
save: 'Save',
},
error: {
noAreaError: 'Header menu space element is required.',
}, },
license: { license: {
expiry: { expiry: {
title: 'License Expired - {domain} - {plugin}', expiryDate: 'Expiry Date {date}'
message: 'Your plugin license has expired. To continue using this feature, please purchase a new license.',
domain: 'Domain',
plugin: 'Plugin',
expiryDate: 'Expiry Date'
}, },
button: { button: {
purchase: 'Purchase License', purchase: {
checkLicense: 'Check Again', label: 'Purchase',
close: 'Close' desc: 'All applications under this domain can be used.'
},
checkLicense: {
label: 'Check',
desc: 'Recheck the license status.'
},
hide: 'Hide'
}, },
notification: { notification: {
dontShowAgain: 'Don\'t show again' days: 'today | tomorrow | {count} days later',
}, gotoLink: 'Please visit <a class="notification-link" href="{link}">Plugin Setting Page</a>.',
permission: { expired: 'Your plugin {plugin} license has expired.<br>@:license.notification.gotoLink',
noAccess: 'You do not have permission to access this feature', warning: 'Your plugin {plugin} license will be expired {days}.<br>@:license.notification.gotoLink'
adminRequired: 'Administrator privileges required'
}, },
status: { status: {
label: 'License Status: ',
checking: 'Checking license...', checking: 'Checking license...',
valid: 'License is valid', expired: 'Expired',
expired: 'License has expired', expiredDisplay: 'License has expired',
expiringSoon: 'License expires soon', permanent: 'Permanent license',
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 { export default {
hello: "こんにちは!", hello: "こんにちは!",
@@ -8,38 +11,53 @@ export default {
button: { button: {
label: 'ボタン名', label: 'ボタン名',
default: 'ボタン', default: 'ボタン',
error: {
required: '@:config.button.labelを入力してください。'
}
}, },
message: { message: {
label: 'メッセージ', label: 'メッセージ',
default: 'こんにちは、世界', default: '',
}, },
cancel: 'キャンセル',
save: '保存する',
},
error: {
noAreaError: 'このページではヘッダー要素が利用できません。',
}, },
license: { license: {
expiry: { expiry: {
title: 'ライセンスの有効期限が切れました - {domain} - {plugin}', expiryDate: '有効期限 {date} ',
message: 'プラグインのライセンス有効期限が切れています。この機能を引き続き使用するには、新しいライセンスを購入してください。',
domain: 'ドメイン',
plugin: 'プラグイン',
expiryDate: '有効期限'
}, },
button: { button: {
purchase: 'ライセンスを購入', purchase: {
checkLicense: '再確認', label: '購入',
close: '閉じる' desc: '購入後、このドメイン下の全アプリで利用可能'
},
checkLicense: {
label: '再確認',
desc: 'ライセンス状態を再確認'
},
hide: '非表示'
}, },
notification: { notification: {
dontShowAgain: '今後表示しない' days: '今日 | 明日 | {count}日後',
}, gotoLink: 'ご利用には<a class="notification-link" href="{link}">「プラグインの設定」</a>をご確認ください。',
permission: { expired: 'プラグイン「{plugin}」の有効期限が切れました。<br>@:license.notification.gotoLink',
noAccess: 'この機能にアクセスする権限がありません', warning: 'プラグイン「{plugin}」の試用期間が{days}で終了します。<br>@:license.notification.gotoLink'
adminRequired: '管理者権限が必要です'
}, },
status: { status: {
checking: 'ライセンスを確認しています...', label: 'ライセンス状態: ',
valid: 'ライセンスは有効です', checking: '確認しています...',
expired: 'ライセンスの有効期限切れました', expired: '期限切れ',
expiringSoon: 'ライセンスの有効期限が近づいています', expiredDisplay: 'ライセンスの有効期限が切れました',
permanent: '永久ライセンス' permanent: '永久ライセンス',
permanentDisplay: '永久利用可能',
unknown: '不明'
},
error: {
fetchFailed: 'リモートライセンスチェックに失敗しました: {e}',
checkFailed: 'ライセンスチェックに失敗しました。プラグイン機能が無効になりました: {e}'
} }
}, },
}; };

View File

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

View File

@@ -6,60 +6,55 @@ import { MobileButton } from 'kintone-ui-component/lib/mobile/button';
(function (PLUGIN_ID) { (function (PLUGIN_ID) {
kintone.events.on('mobile.app.record.index.show', () => { kintone.events.on('mobile.app.record.index.show', () => {
// 授权了才能使用 // 授权了才能使用
LicenseService.loadPluginIfAuthorized(PLUGIN_ID, LicenseService.loadPluginIfAuthorized(PLUGIN_ID,
async () => { async () => {
// 获取当前应用ID // 获取当前应用ID
const appIdNum = kintone.mobile.app.getId(); const appIdNum = kintone.mobile.app.getId();
if (!appIdNum) { if (!appIdNum) {
return; return;
}; };
const appId = appIdNum.toString(); 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'; const btnId = 'template-btn-id';
if (document.getElementById(btnId)) { if (document.getElementById(btnId)) {
return; return;
}; };
// 获取 Header 容器元素 // 测试 i18n
const headerSpace = kintone.mobile.app.getHeaderSpaceElement(); const { t } = i18n.global;
if (!headerSpace) {
throw new Error('このページではヘッダー要素が利用できません。'); // 获取 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 headerSpace.appendChild(button);
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天就到期了请及时续费。',
}, },
); );
}); });

View File

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