improve UIUX

This commit is contained in:
2025-10-23 14:32:39 +08:00
parent 7a3eb40aa9
commit 8565b9513d
8 changed files with 285 additions and 98 deletions

View File

@@ -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
View File

@@ -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']
}

View File

@@ -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);
}
}
}
/**
* 取消操作并返回插件列表
* 重定向到插件管理页面

View 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>

View File

@@ -15,7 +15,7 @@
/* 最上面的说明 */
.settings-heading {
padding: 1em 0;
margin-bottom: 8px;
margin: 8px 0;
}
/* label 样式 */

View File

@@ -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;
}

View File

@@ -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(),

View File

@@ -17,6 +17,7 @@ export interface LicenseInfo {
export interface LicenseSetting {
suppressMsgTime?: number;
hideLicenseAreaIfPaid?: boolean;
}
export interface LicenseCheckResult {