improve UIUX
This commit is contained in:
18
README.md
18
README.md
@@ -422,4 +422,20 @@ watch(loading, (load) => {
|
||||
load ? spinner.value?.open() : spinner.value?.close();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
```
|
||||
|
||||
|
||||
### 关于 tooltip
|
||||
|
||||
kuc 没有实现插槽,所以应该当成一个普通组件使用:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<button ref="buyButton">购买</button>
|
||||
<kuc-tooltip title="点击购买" :container="buyButton"></kuc-tooltip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const buyButton = shallowRef<HTMLButtonElement | null>(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -8,6 +8,7 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Config: typeof import('./src/components/Config.vue')['default']
|
||||
LicenseStatus: typeof import('./src/components/LicenseStatus.vue')['default']
|
||||
PluginInput: typeof import('./src/components/basic/PluginInput.vue')['default']
|
||||
PluginLabel: typeof import('./src/components/basic/PluginLabel.vue')['default']
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<template>
|
||||
<!-- 许可证状态信息 -->
|
||||
<LicenseStatus />
|
||||
|
||||
<h2 class="settings-heading">{{ $t('config.title') }}</h2>
|
||||
<p class="kintoneplugin-desc">{{ $t('config.desc') }}</p>
|
||||
|
||||
@@ -9,7 +12,7 @@
|
||||
|
||||
<div class="action-area">
|
||||
<kuc-button text="キャンセル" type="normal" @click="cancel" />
|
||||
<kuc-button :disabled="loading" text="保存する" class="save-btn" type="submit" @click="save" />
|
||||
<kuc-button text="保存する" class="save-btn" type="submit" @click="save" />
|
||||
</div>
|
||||
|
||||
<!-- 必须使用 v-show 让 spinner dom 创建的时候不显示 -->
|
||||
@@ -19,10 +22,8 @@
|
||||
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 LicenseStatus from './LicenseStatus.vue';
|
||||
|
||||
import { nextTick, onMounted, reactive, ref, shallowRef, watch } from 'vue';
|
||||
|
||||
@@ -44,21 +45,11 @@ 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');
|
||||
@@ -70,10 +61,8 @@ onMounted(async () => {
|
||||
console.log('pluginsInfo', pluginsInfo);
|
||||
// 模拟加载时间,展示 spinner 效果
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
loading.value = false;
|
||||
|
||||
// 检查许可证状态,显示相关信息
|
||||
checkLicenseStatus();
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,54 +104,6 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消操作并返回插件列表
|
||||
* 重定向到插件管理页面
|
||||
|
||||
151
src/components/LicenseStatus.vue
Normal file
151
src/components/LicenseStatus.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div v-if="shown" class="license-status-info">
|
||||
<div class="license-content">
|
||||
<div class="license-status-text">
|
||||
<strong>许可证状态: </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>
|
||||
</template>
|
||||
<button v-else class="action-btn" @click="hidePaidMsg">不再显示</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, shallowRef } from 'vue';
|
||||
import { LicenseService } from '@/services/LicenseService';
|
||||
import type { LicenseInfo, LicenseCheckResult } from '@/types/license';
|
||||
import { LicenseStorage } from '@/utils/LicenseStorage';
|
||||
|
||||
type LicenseDisplayInfo = {
|
||||
isPaid: boolean;
|
||||
expiryDate: string;
|
||||
isExpired: boolean;
|
||||
remainingDays: number;
|
||||
};
|
||||
|
||||
// 状态管理
|
||||
const licenseInfo = ref<LicenseInfo | undefined>();
|
||||
const loading = ref(true);
|
||||
const shown = ref(false);
|
||||
const checking = ref(false);
|
||||
const checkButton = shallowRef<HTMLButtonElement | null>(null);
|
||||
const buyButton = shallowRef<HTMLButtonElement | null>(null);
|
||||
|
||||
// 许可证显示信息
|
||||
const licenseDisplayInfo = computed<LicenseDisplayInfo>(() => {
|
||||
if (!licenseInfo.value || loading.value) {
|
||||
return {
|
||||
isPaid: false,
|
||||
expiryDate: '未知',
|
||||
isExpired: false,
|
||||
remainingDays: 1,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isPaid: licenseInfo.value.isPaid,
|
||||
expiryDate: LicenseService.formatExpiryDate(licenseInfo.value.expiredTime),
|
||||
isExpired: licenseInfo.value.expiredTime < Date.now() && !licenseInfo.value.isPaid,
|
||||
remainingDays: LicenseService.getDaysRemaining(licenseInfo.value.expiredTime),
|
||||
};
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const result = await LicenseService.checkLicense();
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
licenseInfo.value = result.license;
|
||||
|
||||
shown.value = isPaidAreaShown();
|
||||
if (!shown.value) {
|
||||
addPaidLabel();
|
||||
}
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const addPaidLabel = () => {
|
||||
const isExist = !!document.querySelector('#license-label')
|
||||
if (!shown.value && !isExist) {
|
||||
const spanElement = document.createElement('span');
|
||||
spanElement.textContent = '已授权';
|
||||
spanElement.id = 'license-label';
|
||||
document.querySelector('#app > .settings-heading')?.appendChild(spanElement);
|
||||
}
|
||||
}
|
||||
|
||||
const isPaidAreaShown = () => {
|
||||
if (!licenseInfo.value?.isPaid) {
|
||||
return true;
|
||||
}
|
||||
const settings = LicenseStorage.getSettings(licenseInfo.value.pluginId);
|
||||
if (settings.hideLicenseAreaIfPaid) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const licenseStatusText = computed(() => {
|
||||
if (!licenseDisplayInfo.value) {
|
||||
return '未知';
|
||||
}
|
||||
|
||||
if (loading.value) {
|
||||
return '加载中...';
|
||||
}
|
||||
|
||||
if (licenseDisplayInfo.value.isPaid) {
|
||||
return '<span class="text-green">永久可用</span>';
|
||||
}
|
||||
|
||||
let status = `到期时间 ${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 + ' 天)';
|
||||
}
|
||||
|
||||
return status;
|
||||
});
|
||||
|
||||
async function refreshLicenseStatus() {
|
||||
checking.value = true;
|
||||
loading.value = true;
|
||||
// 模拟加载时间,展示 spinner 效果
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
const result = await LicenseService.forceRefreshLicense();
|
||||
licenseInfo.value = result.license;
|
||||
loading.value = false;
|
||||
checking.value = false;
|
||||
}
|
||||
|
||||
function purchaseLicense() {
|
||||
alert(
|
||||
`请联系管理员或访问官方网站购买许可证。\nDomain: ${licenseInfo.value?.domain}\nPlugin: ${licenseInfo.value?.pluginId}`,
|
||||
);
|
||||
}
|
||||
|
||||
function hidePaidMsg() {
|
||||
const pluginId = licenseInfo.value?.pluginId;
|
||||
if (!pluginId) {
|
||||
return;
|
||||
}
|
||||
const settings = LicenseStorage.getSettings(pluginId);
|
||||
settings.hideLicenseAreaIfPaid = true;
|
||||
LicenseStorage.saveSetting(settings, pluginId);
|
||||
shown.value = false;
|
||||
addPaidLabel();
|
||||
}
|
||||
</script>
|
||||
@@ -15,7 +15,7 @@
|
||||
/* 最上面的说明 */
|
||||
.settings-heading {
|
||||
padding: 1em 0;
|
||||
margin-bottom: 8px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
/* label 样式 */
|
||||
|
||||
@@ -271,27 +271,99 @@
|
||||
}
|
||||
|
||||
.license-status-info {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e5e5;
|
||||
background-color: #f7fafc;
|
||||
padding: 12px 16px;
|
||||
margin-right: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f9f9f9;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.license-status-info .license-info h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #2d3748;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
.license-status-info .license-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.license-status-info .license-info p {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
color: #4a5568;
|
||||
line-height: 1.4;
|
||||
.license-status-info .license-status-text {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.license-status-info .license-info p:last-child {
|
||||
margin-bottom: 0;
|
||||
.license-status-info .license-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn {
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 65px;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn:not(.main) {
|
||||
background: transparent;
|
||||
color: #666;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn:not(.main):disabled {
|
||||
background: #efefef;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn:not(.main):not(:disabled):hover {
|
||||
background: #eee;
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn:not(.main):not(:disabled):active {
|
||||
background: #BDBDBD;
|
||||
border-color: #757575;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn.main {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: 1px solid #4CAF50;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn.main:hover {
|
||||
background: #388E3C;
|
||||
border-color: #388E3C;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn.main:active {
|
||||
background: #33691E;
|
||||
border-color: #33691E;
|
||||
}
|
||||
|
||||
.notification-link {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notification-link:hover {
|
||||
color: #daf0ff;
|
||||
}
|
||||
|
||||
.text-red {
|
||||
color: #B71C1C;
|
||||
}
|
||||
|
||||
.text-green {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
#license-label {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 9999px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { MobileNotification } from 'kintone-ui-component/lib/mobile/notification
|
||||
export class LicenseService {
|
||||
// 常量定义
|
||||
private static readonly WARNING_DAYS_BEFORE_EXPIRY = 7;
|
||||
private static readonly TRIAL_DATE = 1;
|
||||
private static readonly TRIAL_DATE = 30;
|
||||
private static PLUGIN_ID: string = '';
|
||||
// ============ 基础工具函数 ============
|
||||
|
||||
@@ -22,7 +22,7 @@ export class LicenseService {
|
||||
* 获取插件ID
|
||||
*/
|
||||
static getPluginId(): string {
|
||||
return this.PLUGIN_ID;
|
||||
return this.PLUGIN_ID || new URLSearchParams(window.location.search).get('pluginId') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,7 +122,7 @@ export class LicenseService {
|
||||
*/
|
||||
static formatExpiryDate(timestamp: number): string {
|
||||
if (timestamp === -1) return '永久';
|
||||
return new Date(timestamp).toLocaleDateString('zh-CN');
|
||||
return new Date(timestamp).toLocaleDateString(kintone.getLoginUser().language);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +132,7 @@ export class LicenseService {
|
||||
if (timestamp === -1) return Infinity;
|
||||
const now = new Date().getTime();
|
||||
const remainingMs = timestamp - now;
|
||||
return Math.ceil(remainingMs / (24 * 60 * 60 * 1000));
|
||||
return Math.floor(remainingMs / (24 * 60 * 60 * 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,28 +167,32 @@ export class LicenseService {
|
||||
warningDialogMessage?: string;
|
||||
}) {
|
||||
const remainingDays = this.getDaysRemaining(license.expiredTime);
|
||||
const msg = remainingDays > 0 ? ` ${remainingDays} 天后` : '今天'
|
||||
const defaultMessage = `您的插件许可证将在${msg}到期,请及时续期以避免服务中断。`;
|
||||
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 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());
|
||||
// const settings = LicenseStorage.getSettings(this.getPluginId());
|
||||
// if (settings.suppressMsgTime && this.isToday(settings.suppressMsgTime)) {
|
||||
// return
|
||||
// }
|
||||
// delete settings.suppressMsgTime
|
||||
// LicenseStorage.saveSetting(settings, this.getPluginId());
|
||||
|
||||
if (await kintone.isMobilePage()) {
|
||||
const notification = new MobileNotification({
|
||||
text: message,
|
||||
content: message,
|
||||
});
|
||||
notification.open();
|
||||
} else {
|
||||
const notification = new Notification({
|
||||
text: message,
|
||||
content: message,
|
||||
type: 'info',
|
||||
});
|
||||
notification.open();
|
||||
@@ -279,6 +283,7 @@ export class LicenseService {
|
||||
private static mockCreateTrialLicense(): LicenseInfo {
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setDate(expiryDate.getDate() + this.TRIAL_DATE);
|
||||
expiryDate.setHours(23, 59, 59, 999);
|
||||
|
||||
return {
|
||||
expiredTime: expiryDate.getTime(),
|
||||
|
||||
1
src/types/license.d.ts
vendored
1
src/types/license.d.ts
vendored
@@ -17,6 +17,7 @@ export interface LicenseInfo {
|
||||
|
||||
export interface LicenseSetting {
|
||||
suppressMsgTime?: number;
|
||||
hideLicenseAreaIfPaid?: boolean;
|
||||
}
|
||||
|
||||
export interface LicenseCheckResult {
|
||||
|
||||
Reference in New Issue
Block a user