Compare commits
15 Commits
2472f7bcf9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c55323d47d | |||
| 3198d6f646 | |||
| c4999c4804 | |||
| 63a2cfc186 | |||
| 393c637163 | |||
| fc3249987e | |||
| 308291825d | |||
| a4ae06ea52 | |||
| 767af75aad | |||
| e86afd8487 | |||
| 3961285b32 | |||
| e531b50a0d | |||
| 8565b9513d | |||
| 7a3eb40aa9 | |||
| 6649680296 |
112
README.md
112
README.md
@@ -1,8 +1,10 @@
|
|||||||
|
|
||||||
# kintone-vue-ts-template
|
# kintone-vue-template
|
||||||
|
|
||||||
使用 Vue 、ts 和 Vite 创建 kintone plugin 的初始化模板,先由 [create-plugin](https://cybozu.dev/ja/kintone/sdk/development-environment/create-plugin/) 生成之后再手动引入 Vue。
|
使用 Vue 、ts 和 Vite 创建 kintone plugin 的初始化模板,先由 [create-plugin](https://cybozu.dev/ja/kintone/sdk/development-environment/create-plugin/) 生成之后再手动引入 Vue。
|
||||||
|
|
||||||
|
并且提供了 [License 检查](#license-检查) 的功能。
|
||||||
|
|
||||||
> プラグイン開発手順:https://cybozu.dev/ja/kintone/tips/development/plugins/development-plugin/
|
> プラグイン開発手順:https://cybozu.dev/ja/kintone/tips/development/plugins/development-plugin/
|
||||||
|
|
||||||
包括了以下 kintone 库:
|
包括了以下 kintone 库:
|
||||||
@@ -76,6 +78,38 @@ npm install # 或 yarn
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
3. 如果需要使用 License 检查,请参考: [License 检查](#license-检查)
|
||||||
|
|
||||||
|
需要修改 `src/config.json`:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
{
|
||||||
|
"license": {
|
||||||
|
"enabled": true, // 是否开启 License 检查
|
||||||
|
"api": {
|
||||||
|
"checkUrl": "https://kintone.alicorns.co.jp/api/license/check", // server 路径,一般不需要修改
|
||||||
|
+ "pluginKey": 插件的 id,需要和后端数据库中保持一致
|
||||||
|
- "pluginKey": "kintone-vue-template"
|
||||||
|
},
|
||||||
|
"purchase": {
|
||||||
|
+ "url": 购买页面打开的链接
|
||||||
|
- "url": "https://alisurvey.alicorns.co.jp/s/Iuc5RxZv",
|
||||||
|
"formIds": {
|
||||||
|
+ "name": 表单中存储申请人的名字的 field id, 会设置为 kintone 的登陆用户名,为空就跳过
|
||||||
|
- "name": "input1761283314263",
|
||||||
|
+ "email": 表单中存储申请人 email 的 field id, 会设置为 kintone 的登陆邮箱,为空就跳过
|
||||||
|
- "email": "input1761283275767",
|
||||||
|
+ "domain": 表单中存储用户 url domain 的 field id, 会设置为 url
|
||||||
|
- "domain": "input1761283180784",
|
||||||
|
+ "pluginId": 表单中存储用户 plugin id 的 field id, 会设置为 plugin id
|
||||||
|
- "pluginId": "input1761283200616"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warningDaysBeforeExpiry": 7 // 进行提醒的剩余天数
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
# 编译流程
|
# 编译流程
|
||||||
|
|
||||||
@@ -161,6 +195,8 @@ npm run build-upload # 或 yarn build-upload
|
|||||||
├── package.json
|
├── package.json
|
||||||
├── private.ppk # 当前 plugin 密钥,首次 build 自动生成
|
├── private.ppk # 当前 plugin 密钥,首次 build 自动生成
|
||||||
├── README.md
|
├── README.md
|
||||||
|
├── rsa_private.pem # license 验证密钥
|
||||||
|
├── rsa_public.pem # license 公钥,需要被添加到密钥检查的后端系统中
|
||||||
├── tsconfig.json
|
├── tsconfig.json
|
||||||
└── vite.config.ts # 主要的 vite 配置
|
└── vite.config.ts # 主要的 vite 配置
|
||||||
└── vite.iife.config.ts # 用于打包 desktop/mobile 文件的配置
|
└── vite.iife.config.ts # 用于打包 desktop/mobile 文件的配置
|
||||||
@@ -214,9 +250,8 @@ desktop/mobile 的 js 会被 `vite` 使用 `lib` 模式打包,从而将所有
|
|||||||
在 desktop/mobile,直接使用:
|
在 desktop/mobile,直接使用:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
import client from '@/plugins/kintoneClient.ts'
|
||||||
|
|
||||||
const client = new KintoneRestAPIClient();
|
|
||||||
client.app...
|
client.app...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -230,7 +265,6 @@ client.app...
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 关于 i18n
|
## 关于 i18n
|
||||||
|
|
||||||
在 desktop/mobile:
|
在 desktop/mobile:
|
||||||
@@ -424,3 +458,73 @@ watch(loading, (load) => {
|
|||||||
});
|
});
|
||||||
</script>
|
</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>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License 检查
|
||||||
|
|
||||||
|
目前 License 的检查使用的是后端传一个 RSA 密钥加密的 JWT,前端使用公钥验证是否被修改过,并且保存在 Localstorage 中。
|
||||||
|
|
||||||
|
每隔一天都会从后端重新获取新的 License 信息。
|
||||||
|
|
||||||
|
默认试用是 30 天,还剩 7 天的时候会进行提醒。(在 `license-service.ts` 中修改)
|
||||||
|
|
||||||
|
当点击购买的时候,会跳转到一个外部网站,这里可以使用 AliSurvey 进行表单填写。
|
||||||
|
|
||||||
|
|
||||||
|
### 创建密钥
|
||||||
|
|
||||||
|
创建密钥可以使用 openssl 命令行工具:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
openssl genrsa -out ./rsa_private.pem 2048
|
||||||
|
```
|
||||||
|
|
||||||
|
然后公钥可以放在同一个文件夹中:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
openssl rsa -in ./rsa_private.pem -pubout -out ./rsa_public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
> 公钥需要被放到后端系统中
|
||||||
|
|
||||||
|
|
||||||
|
### 使用
|
||||||
|
|
||||||
|
在 desktop/mobile 中只要在所有的逻辑外部多包裹一层 `LicenseService.loadPluginIfAuthorized()` :
|
||||||
|
|
||||||
|
```ts
|
||||||
|
kintone.events.on('app.record.index.show', async () => {
|
||||||
|
LicenseService.loadPluginIfAuthorized(PLUGIN_ID, // <--- 内部会进行 license 判断
|
||||||
|
async () => {
|
||||||
|
// 已有的逻辑代码
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
在 vue 中只要在顶部引入 ` LicenseStatus.vue`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 许可证状态信息 -->
|
||||||
|
<LicenseStatus />
|
||||||
|
|
||||||
|
<h2 class="settings-heading">{{ $t('config.title') }}</h2>
|
||||||
|
```
|
||||||
|
|||||||
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -8,6 +8,7 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
Config: typeof import('./src/components/Config.vue')['default']
|
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']
|
PluginInput: typeof import('./src/components/basic/PluginInput.vue')['default']
|
||||||
PluginLabel: typeof import('./src/components/basic/PluginLabel.vue')['default']
|
PluginLabel: typeof import('./src/components/basic/PluginLabel.vue')['default']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,15 @@
|
|||||||
"build": "vite build && npm run pkg",
|
"build": "vite build && npm run pkg",
|
||||||
"build:prod": "cross-env BUILD_MODE=production vite build && npm run pkg",
|
"build:prod": "cross-env BUILD_MODE=production vite build && npm run pkg",
|
||||||
"build-upload": "npm run build && npm run upload",
|
"build-upload": "npm run build && npm run upload",
|
||||||
|
"build-upload:prod": "npm run build:prod && npm run upload",
|
||||||
"pkg": "kintone-plugin-packer --ppk private.ppk --out dist/plugin.zip dist/src",
|
"pkg": "kintone-plugin-packer --ppk private.ppk --out dist/plugin.zip dist/src",
|
||||||
"upload": "kintone-plugin-uploader --base-url https://alicorn.cybozu.com --username maxz --password 7ld7i8vd dist/plugin.zip "
|
"upload": "kintone-plugin-uploader --base-url https://alicorn.cybozu.com --username maxz --password 7ld7i8vd dist/plugin.zip "
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kintone/rest-api-client": "^5.7.5",
|
"@kintone/rest-api-client": "^5.7.5",
|
||||||
"kintone-ui-component": "1.22.0",
|
"jsrsasign": "^11.1.0",
|
||||||
|
"jsrsasign-util": "^1.0.5",
|
||||||
|
"kintone-ui-component": "1.23.0",
|
||||||
"rollup-plugin-css-only": "^4.5.2",
|
"rollup-plugin-css-only": "^4.5.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-i18n": "^11.0.1"
|
"vue-i18n": "^11.0.1"
|
||||||
@@ -23,6 +26,7 @@
|
|||||||
"@kintone/dts-gen": "^8.1.1",
|
"@kintone/dts-gen": "^8.1.1",
|
||||||
"@kintone/plugin-packer": "^8.1.3",
|
"@kintone/plugin-packer": "^8.1.3",
|
||||||
"@kintone/plugin-uploader": "^9.1.2",
|
"@kintone/plugin-uploader": "^9.1.2",
|
||||||
|
"@types/jsrsasign": "^10.5.15",
|
||||||
"@types/node-rsa": "^1.1.4",
|
"@types/node-rsa": "^1.1.4",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vue/tsconfig": "^0.7.0",
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
|||||||
28
rsa_private.pem
Normal file
28
rsa_private.pem
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDOAqDRabJn6vga
|
||||||
|
YXnWuYKPomQVNV0R0HPmuN1N4qo7OMDkFH2mln86w68/9/bHMb7TcVaFGxNblpe2
|
||||||
|
tIjr/u/XfnS2+NK6ckTWOhmPUrqLG6A3a4v49dymvFdfyjAYOCBTlKzrdQTjdFAX
|
||||||
|
5EUE007n/G+RxYiEAm0NXpVqa+aw/Yvm5+Lr7GrKpyamAAygu5jYDE3rateyfJ1h
|
||||||
|
P2fEXEqR4RPD5pgyKD+4ZVbygaYqAw4rZu4OOZ4QtPCtksSenn94Yx9UUlex3TNW
|
||||||
|
oTBrFp+8z9wjg+oF6/5Ai/PsXLDpbkIusnQo1yYvrULwaAHBvL6wgj1FOL+g6Tmp
|
||||||
|
P6MhU7EbAgMBAAECggEAAyia0vXQv0jPgAa26a9ozI9RWIU7xZOmy0anV1Peh0+X
|
||||||
|
SYf05efQez6tLQkTbA9xB73o9TEqlVC+8mqx32hxFffsOIf3zIBcWiqEN3nYaVya
|
||||||
|
6BlKiUkqa8C0guqkh80zKwTlt9XQpYokK5GblfeFcgd2pctbt1Ea30XFewm80kGH
|
||||||
|
OrHP4bi25KYkS+6PL5wUI+OqfaR/LsWevwt+cLlZPGmDDgE6+2Bvcg0JWnKKwk1k
|
||||||
|
jhKpSAek6zUkweaysxx1BscNBvGtDnRRRy0+2jMK66ad80Cg0G8xLQ/tu8kBwfml
|
||||||
|
T/9ViSRUjUrMdcfSVkrdOV3xAsOXdWspz1aljJAwcQKBgQDxebBimIBgUhNJ+2/9
|
||||||
|
27OQXsHLBYbqPurR4fKCM7a5qAIpXJcgth0YAuNvJT+rxEHTPypQCTF1fvvJuU+v
|
||||||
|
w1HjUz1CHzGYWdj0xibQDdtgkPlKqQQeT3Cnpwy3RkT4IF6FCH9YfKcUzYdamVZy
|
||||||
|
S8kFqLzEStwWIoaQX2HoBZLoUQKBgQDaZtZJoIHr+d0fLz+7rAoB8WEBA8lOaEhU
|
||||||
|
/OHwrUbuAez6k7zEDsTXgLuB8N34p1yMSaOqdGJ8rj3Pg8AlrXGELfXOF7Fe9zG0
|
||||||
|
LxujNmftmJKE8XTIALPpSBJgF5LDnh13LcNMd4b6w0DDjF7wCA2T3DTLjjqi3fo7
|
||||||
|
nDW9WQyTqwKBgEXlbXL8pZw75a1yhHY80/skEoBLt0OytpHODz407d1LjmSekng7
|
||||||
|
fqxmmaKga4+ynUMic4L7Rj+2Y/d+FlzP8rIUdBThpp9s0mn3uWBbwnZvQFmmFrUX
|
||||||
|
VYqRxhJ+2pPf+rwTO5lHa62P2HAXFni7CxMCRrGi4ZXepIjBsztP8bghAoGBAMj5
|
||||||
|
Bsmb1NJkDBGNNhWpm0/sYbpAVLc9CQqD5hnGKdYMmZh/6J11hbdVM7bAAlK1F1nU
|
||||||
|
zbGmBZb7888ISwGg2Cus61tpvANKb0eCbelDwGEIHBQP6Mm+s8/ATYB1UM2Hq0+n
|
||||||
|
Iec0ulX45JjNi/NPRcdBRKfnypdissi111HVJtifAoGADrT/t2j1bMHd1zY76Jv5
|
||||||
|
M/BWhOAgHwBo683ROEL3p6G98Xla2kd7hP6NEdo6OLJnlfK3RfoPWnotLZKEsfQM
|
||||||
|
Wog9GAcYtQbDDaYWxNBgyscSdVousY1EXs+t5vkt3XSIwUwLcEVJOu7/Fvt+YXpn
|
||||||
|
nR7aD8F62ARYxI+bv+bQR5w=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
9
rsa_public.pem
Normal file
9
rsa_public.pem
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzgKg0WmyZ+r4GmF51rmC
|
||||||
|
j6JkFTVdEdBz5rjdTeKqOzjA5BR9ppZ/OsOvP/f2xzG+03FWhRsTW5aXtrSI6/7v
|
||||||
|
1350tvjSunJE1joZj1K6ixugN2uL+PXcprxXX8owGDggU5Ss63UE43RQF+RFBNNO
|
||||||
|
5/xvkcWIhAJtDV6VamvmsP2L5ufi6+xqyqcmpgAMoLuY2AxN62rXsnydYT9nxFxK
|
||||||
|
keETw+aYMig/uGVW8oGmKgMOK2buDjmeELTwrZLEnp5/eGMfVFJXsd0zVqEwaxaf
|
||||||
|
vM/cI4PqBev+QIvz7Fyw6W5CLrJ0KNcmL61C8GgBwby+sII9RTi/oOk5qT+jIVOx
|
||||||
|
GwIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<!-- 许可证状态信息 -->
|
||||||
|
<LicenseStatus />
|
||||||
|
|
||||||
<h2 class="settings-heading">{{ $t('config.title') }}</h2>
|
<h2 class="settings-heading">{{ $t('config.title') }}</h2>
|
||||||
<p class="kintoneplugin-desc">{{ $t('config.desc') }}</p>
|
<p class="kintoneplugin-desc">{{ $t('config.desc') }}</p>
|
||||||
|
|
||||||
@@ -8,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 :disabled="loading" 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 创建的时候不显示 -->
|
||||||
@@ -20,6 +23,7 @@ import type { Setting } from '@/types';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useKintoneClient } from '@/composables/useKintoneClient';
|
import { useKintoneClient } from '@/composables/useKintoneClient';
|
||||||
import type { Spinner } from 'kintone-ui-component';
|
import type { Spinner } from 'kintone-ui-component';
|
||||||
|
import LicenseStatus from './LicenseStatus.vue';
|
||||||
|
|
||||||
import { nextTick, onMounted, reactive, ref, shallowRef, watch } from 'vue';
|
import { nextTick, onMounted, reactive, ref, shallowRef, watch } from 'vue';
|
||||||
|
|
||||||
@@ -45,17 +49,22 @@ onMounted(async () => {
|
|||||||
// 等待页面完全渲染后再显示加载状态,实现更平滑的用户体验
|
// 等待页面完全渲染后再显示加载状态,实现更平滑的用户体验
|
||||||
nextTick(async () => {
|
nextTick(async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
// 获取已保存的插件配置
|
// 获取已保存的插件配置
|
||||||
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));
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -73,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;
|
||||||
|
|||||||
180
src/components/LicenseStatus.vue
Normal file
180
src/components/LicenseStatus.vue
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="shown" class="license-status-info">
|
||||||
|
<div class="license-content">
|
||||||
|
<div class="license-status-text">
|
||||||
|
<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">{{ $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') }}
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="open-icon">
|
||||||
|
<path d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<kuc-tooltip :title="$t('license.button.purchase.desc')" :container="buyButton"></kuc-tooltip>
|
||||||
|
</template>
|
||||||
|
<button v-else class="action-btn" @click="hidePaidMsg">{{ $t('license.button.hide') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed, shallowRef } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { LicenseService } from '@/services/license-service';
|
||||||
|
import type { LicenseInfo } from '@/types/license';
|
||||||
|
import { LicenseStorage } from '@/utils/license-storage';
|
||||||
|
import config from '@/config.json';
|
||||||
|
|
||||||
|
const { t: $t } = useI18n();// 配置国际化
|
||||||
|
|
||||||
|
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: $t('license.status.unknown'),
|
||||||
|
isExpired: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
if (!config.license.enabled) {
|
||||||
|
// 许可证功能未启用,不显示组件
|
||||||
|
shown.value = false;
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = $t('license.status.permanent'),
|
||||||
|
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 $t('license.status.unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading.value) {
|
||||||
|
return $t('license.status.checking');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (licenseDisplayInfo.value.isPaid) {
|
||||||
|
return `<span class="text-green">${$t('license.status.permanentDisplay')}</span>`;
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
let status = $t('license.expiry.expiryDate', { date: licenseDisplayInfo.value.expiryDate });
|
||||||
|
|
||||||
|
if (licenseDisplayInfo.value.isExpired) {
|
||||||
|
status += `(<span class="text-red">${$t('license.status.expired')}</span>)`;
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingDays = licenseDisplayInfo.value.remainingDays;
|
||||||
|
if (remainingDays !== undefined) {
|
||||||
|
const days = $t('license.notification.days', remainingDays)
|
||||||
|
status += `(${days})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
const { name, email } = kintone.getLoginUser();
|
||||||
|
const domain = licenseInfo.value?.domain;
|
||||||
|
const pluginId = licenseInfo.value?.pluginId;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
name: { inputId: config.license.purchase.formIds.name, value: name },
|
||||||
|
email: { inputId: config.license.purchase.formIds.email, value: email },
|
||||||
|
domain: { inputId: config.license.purchase.formIds.domain, value: domain },
|
||||||
|
pluginId: { inputId: config.license.purchase.formIds.pluginId, value: pluginId }
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryParams = Object.values(params)
|
||||||
|
.filter(param => param.value)
|
||||||
|
.map(param => `${param.inputId}=${encodeURIComponent(param.value as string)}`)
|
||||||
|
.join('&');
|
||||||
|
|
||||||
|
window.open(`${config.license.purchase.url}?${queryParams}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
19
src/config.json
Normal file
19
src/config.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"license": {
|
||||||
|
"enabled": true,
|
||||||
|
"api": {
|
||||||
|
"checkUrl": "https://kintone.alicorns.co.jp/api/license/check",
|
||||||
|
"pluginKey": "kintone-vue-template"
|
||||||
|
},
|
||||||
|
"purchase": {
|
||||||
|
"url": "https://alisurvey.alicorns.co.jp/s/Iuc5RxZv",
|
||||||
|
"formIds": {
|
||||||
|
"name": "input1761283314263",
|
||||||
|
"email": "input1761283275767",
|
||||||
|
"domain": "input1761283180784",
|
||||||
|
"pluginId": "input1761283200616"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warningDaysBeforeExpiry": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
/* 最上面的说明 */
|
/* 最上面的说明 */
|
||||||
.settings-heading {
|
.settings-heading {
|
||||||
padding: 1em 0;
|
padding: 1em 0;
|
||||||
margin-bottom: 8px;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* label 样式 */
|
/* label 样式 */
|
||||||
|
|||||||
103
src/css/license.css
Normal file
103
src/css/license.css
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
.license-status-info {
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-right: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.license-status-info .license-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.license-status-info .license-status-text {
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.license-status-info .action-btn .open-icon {
|
||||||
|
margin-left: 2px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-link {
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-link:hover {
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -6,12 +6,56 @@ 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: {
|
||||||
|
expiry: {
|
||||||
|
expiryDate: 'Expiry Date {date}'
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
purchase: {
|
||||||
|
label: 'Purchase',
|
||||||
|
desc: 'All applications under this domain can be used.'
|
||||||
|
},
|
||||||
|
checkLicense: {
|
||||||
|
label: 'Check',
|
||||||
|
desc: 'Recheck the license status.'
|
||||||
|
},
|
||||||
|
hide: 'Hide'
|
||||||
|
},
|
||||||
|
notification: {
|
||||||
|
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.',
|
||||||
|
warning: 'Your plugin {plugin} license will be expired {days}.'
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
label: 'License Status: ',
|
||||||
|
checking: 'Checking 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}',
|
||||||
|
jwtFailed: 'JWT verification or parsing failed: {e}',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,64 @@
|
|||||||
// 日语语言包配置
|
// 日语语言包配置
|
||||||
|
|
||||||
|
import { warn } from "vue";
|
||||||
|
|
||||||
// 包含配置页面相关的翻译文本
|
// 包含配置页面相关的翻译文本
|
||||||
export default {
|
export default {
|
||||||
hello: 'こんにちは!',
|
hello: "こんにちは!",
|
||||||
config: {
|
config: {
|
||||||
title: 'kintone Vue テンプレート',
|
title: 'kintone Vue template',
|
||||||
desc: 'kintoneプラグイン作成用 Vue テンプレート',
|
desc: 'kintone Vue template for creating plugin',
|
||||||
button: {
|
button: {
|
||||||
label: 'ボタン名',
|
label: 'ボタン名',
|
||||||
default: 'ボタン',
|
default: 'ボタン',
|
||||||
|
error: {
|
||||||
|
required: '@:config.button.labelを入力してください。'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
label: 'メッセージ',
|
label: 'メッセージ',
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
cancel: 'キャンセル',
|
||||||
|
save: '保存する',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
noAreaError: 'このページではヘッダー要素が利用できません。',
|
||||||
|
},
|
||||||
|
license: {
|
||||||
|
expiry: {
|
||||||
|
expiryDate: '有効期限 {date} ',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
purchase: {
|
||||||
|
label: '購入',
|
||||||
|
desc: '購入後、このドメイン下の全アプリで利用可能'
|
||||||
|
},
|
||||||
|
checkLicense: {
|
||||||
|
label: '再確認',
|
||||||
|
desc: 'ライセンス状態を再確認'
|
||||||
|
},
|
||||||
|
hide: '非表示'
|
||||||
|
},
|
||||||
|
notification: {
|
||||||
|
days: '今日 | 明日 | {count}日後',
|
||||||
|
gotoLink: 'ご利用には「<a class="notification-link" href="{link}">プラグインの設定</a>」をご確認ください。',
|
||||||
|
expired: 'プラグイン「{plugin}」の有効期限が切れました。',
|
||||||
|
warning: 'プラグイン「{plugin}」の試用期間が{days}で終了します。'
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
label: 'ライセンス状態: ',
|
||||||
|
checking: '確認しています...',
|
||||||
|
expired: '期限切れ',
|
||||||
|
expiredDisplay: 'ライセンスの有効期限が切れました',
|
||||||
|
permanent: '永久ライセンス',
|
||||||
|
permanentDisplay: '永久利用可能',
|
||||||
|
unknown: '不明'
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
fetchFailed: 'リモートライセンスチェックに失敗しました: {e}',
|
||||||
|
checkFailed: 'ライセンスチェックに失敗しました。プラグイン機能が無効になりました: {e}',
|
||||||
|
jwtFailed: 'JWTの検証または解析に失敗しました: {e}',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,57 +1,59 @@
|
|||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
import type { Setting } from '@/types';
|
import type { Setting } from '@/types';
|
||||||
|
import { LicenseService } from '@/services/license-service';
|
||||||
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
import client from '@/plugins/kintoneClient.ts'
|
||||||
import { Button } from 'kintone-ui-component/lib/button';
|
import { Button } from 'kintone-ui-component/lib/button';
|
||||||
|
|
||||||
(function (PLUGIN_ID) {
|
(function (PLUGIN_ID) {
|
||||||
kintone.events.on('app.record.index.show', () => {
|
kintone.events.on('app.record.index.show', async () => {
|
||||||
// 获取当前应用ID
|
// 授权了才能使用
|
||||||
const appIdNum = kintone.app.getId();
|
LicenseService.loadPluginIfAuthorized(PLUGIN_ID, async () => {
|
||||||
if (!appIdNum) {
|
// 获取当前应用ID
|
||||||
return;
|
const appIdNum = kintone.app.getId();
|
||||||
};
|
if (!appIdNum) {
|
||||||
const appId = appIdNum.toString();
|
return;
|
||||||
|
|
||||||
// 从插件配置中读取设置信息
|
|
||||||
const setting: Setting = kintone.plugin.app.getConfig(PLUGIN_ID);
|
|
||||||
|
|
||||||
// 检查按钮是否已存在,防止翻页时重复添加
|
|
||||||
const btnId = 'template-btn-id';
|
|
||||||
if (document.getElementById(btnId)) {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取 Header 容器元素
|
|
||||||
const headerSpace = kintone.app.getHeaderMenuSpaceElement();
|
|
||||||
if (!headerSpace) {
|
|
||||||
throw new Error('このページではヘッダー要素が利用できません。');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试 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 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);
|
|
||||||
}
|
}
|
||||||
|
const appId = appIdNum.toString();
|
||||||
|
|
||||||
|
// 从插件配置中读取设置信息
|
||||||
|
const setting: Setting = kintone.plugin.app.getConfig(PLUGIN_ID);
|
||||||
|
|
||||||
|
// 检查按钮是否已存在,防止翻页时重复添加
|
||||||
|
const btnId = 'template-btn-id';
|
||||||
|
if (document.getElementById(btnId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试 i18n
|
||||||
|
const { t } = i18n.global;
|
||||||
|
|
||||||
|
// 获取 Header 容器元素
|
||||||
|
const headerSpace = kintone.app.getHeaderMenuSpaceElement();
|
||||||
|
if (!headerSpace) {
|
||||||
|
throw new Error(t('error.noAreaError'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建按钮
|
||||||
|
const button = new Button({
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
headerSpace.appendChild(button);
|
|
||||||
});
|
});
|
||||||
})(kintone.$PLUGIN_ID);
|
})(kintone.$PLUGIN_ID);
|
||||||
|
|||||||
@@ -1,57 +1,59 @@
|
|||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
import type { Setting } from '@/types';
|
import type { Setting } from '@/types';
|
||||||
|
import { LicenseService } from '@/services/license-service';
|
||||||
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
import client from '@/plugins/kintoneClient.ts'
|
||||||
import { MobileButton } from 'kintone-ui-component/lib/mobile/button';
|
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', () => {
|
||||||
// 获取当前应用ID
|
// 授权了才能使用
|
||||||
const appIdNum = kintone.mobile.app.getId();
|
LicenseService.loadPluginIfAuthorized(PLUGIN_ID, async () => {
|
||||||
if (!appIdNum) {
|
// 获取当前应用ID
|
||||||
return;
|
const appIdNum = kintone.mobile.app.getId();
|
||||||
};
|
if (!appIdNum) {
|
||||||
const appId = appIdNum.toString();
|
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';
|
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('このページではヘッダー要素が利用できません。');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试 i18n
|
// 获取 Header 容器元素
|
||||||
const { t } = i18n.global;
|
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 client = new 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建按钮
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
headerSpace.appendChild(button);
|
|
||||||
});
|
});
|
||||||
})(kintone.$PLUGIN_ID);
|
})(kintone.$PLUGIN_ID);
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"js/desktop.js"
|
"js/desktop.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
|
"css/license.css",
|
||||||
"css/desktop.css"
|
"css/desktop.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"css/51-modern-default.css",
|
"css/51-modern-default.css",
|
||||||
|
"css/license.css",
|
||||||
"css/config.css"
|
"css/config.css"
|
||||||
],
|
],
|
||||||
"required_params": [
|
"required_params": [
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { type App } from 'vue';
|
import { type App } from 'vue';
|
||||||
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
||||||
|
|
||||||
|
// 创建客户端实例(单例)
|
||||||
|
const client = new KintoneRestAPIClient();
|
||||||
|
|
||||||
// 创建注入的 Symbol(用于类型安全)
|
// 创建注入的 Symbol(用于类型安全)
|
||||||
export const KintoneClientInjectionKey = Symbol('kintone-client');
|
export const KintoneClientInjectionKey = Symbol('kintone-client');
|
||||||
|
|
||||||
// 插件安装函数
|
// 插件安装函数
|
||||||
export const kintoneClientPlugin = {
|
export const kintoneClientPlugin = {
|
||||||
install(app: App) {
|
install(app: App) {
|
||||||
// 创建客户端实例(不需要参数)
|
|
||||||
const client = new KintoneRestAPIClient();
|
|
||||||
|
|
||||||
// 提供给整个应用使用
|
// 提供给整个应用使用
|
||||||
app.provide(KintoneClientInjectionKey, client);
|
app.provide(KintoneClientInjectionKey, client);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 导出客户端实例作为默认
|
||||||
|
export default client;
|
||||||
|
|||||||
319
src/services/license-service.ts
Normal file
319
src/services/license-service.ts
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
import type { LicenseInfo, LicenseCheckResult, SavedLicense } from '@/types/license';
|
||||||
|
import i18n from '@/i18n';
|
||||||
|
import { LicenseStorage } from '@/utils/license-storage';
|
||||||
|
import { PermissionService } from '@/utils/permissions';
|
||||||
|
import { Notification } from 'kintone-ui-component/lib/notification';
|
||||||
|
import { MobileNotification } from 'kintone-ui-component/lib/mobile/notification';
|
||||||
|
import manifestJson from '@/manifest.json';
|
||||||
|
import { KJUR } from 'jsrsasign';
|
||||||
|
import rsaPublicKey from '../../rsa_public.pem?raw';
|
||||||
|
import config from '@/config.json';
|
||||||
|
|
||||||
|
const { t: $t } = i18n.global;
|
||||||
|
|
||||||
|
export class LicenseService {
|
||||||
|
// 常量定义
|
||||||
|
private static PLUGIN_ID: string = '';
|
||||||
|
// ============ 基础工具函数 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取域名
|
||||||
|
*/
|
||||||
|
static getDomain(): string {
|
||||||
|
return window.location.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件名
|
||||||
|
*/
|
||||||
|
static getPluginName(): string {
|
||||||
|
const pluginName = manifestJson.name as Record<string, string>;
|
||||||
|
const userLang = kintone.getLoginUser().language;
|
||||||
|
const plugin = pluginName[userLang] || pluginName.ja;
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件ID
|
||||||
|
*/
|
||||||
|
static getPluginId(): string {
|
||||||
|
return this.PLUGIN_ID || new URLSearchParams(window.location.search).get('pluginId') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查许可证是否有效
|
||||||
|
* 参数是解码后的JWT结构,验证签名和payload进行原有检查
|
||||||
|
*/
|
||||||
|
static checkLicenseAvailable(savedLicense: SavedLicense): boolean {
|
||||||
|
try {
|
||||||
|
// 验证完整的JWT
|
||||||
|
const result = KJUR.jws.JWS.verifyJWT(savedLicense.jwt, rsaPublicKey.trim(), { alg: ['RS256'] });
|
||||||
|
if (!result) {
|
||||||
|
console.warn($t('license.error.jwtFailed', { e: '' }));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn($t('license.error.jwtFailed', { e: error }));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const license = savedLicense.licenseInfo;
|
||||||
|
|
||||||
|
const domain = this.getDomain();
|
||||||
|
const pluginId = this.getPluginId();
|
||||||
|
|
||||||
|
// 检查域名和插件ID是否与当前环境一致
|
||||||
|
if (license.domain !== domain || license.pluginId !== pluginId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否付费
|
||||||
|
if (license.isPaid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查存储是否过期(是否同一天)
|
||||||
|
if (!this.isToday(license.fetchTime)) {
|
||||||
|
// 不是同一天,已过期
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查试用是否到期
|
||||||
|
const expiredTime = new Date(license.expiredTime);
|
||||||
|
if (expiredTime < new Date()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static isToday(timestamp: number): boolean {
|
||||||
|
const fetchDate = new Date(timestamp).toDateString();
|
||||||
|
const todayStr = new Date().toDateString();
|
||||||
|
return fetchDate == todayStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 许可证验证
|
||||||
|
*/
|
||||||
|
static async checkLicense(): Promise<LicenseCheckResult> {
|
||||||
|
if (!config.license.enabled || rsaPublicKey.trim() === '') {
|
||||||
|
return {
|
||||||
|
isLicenseValid: true,
|
||||||
|
license: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const localLicense = this.getLocalLicenseInfo() || undefined;
|
||||||
|
if (localLicense) {
|
||||||
|
return {
|
||||||
|
isLicenseValid: true,
|
||||||
|
license: localLicense,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return await this.checkLicenseRemote();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用远程许可证API
|
||||||
|
*/
|
||||||
|
private static async callRemoteLicenseAPI(domain: string, pluginId: string): Promise<string | null> {
|
||||||
|
const url = config.license.api.checkUrl;
|
||||||
|
const method = 'POST';
|
||||||
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
|
const body = {
|
||||||
|
domain,
|
||||||
|
pluginId,
|
||||||
|
pluginKey: config.license.api.pluginKey,
|
||||||
|
};
|
||||||
|
const proxyResponse = await kintone.proxy(url, method, headers, body);
|
||||||
|
if (proxyResponse[1] !== 200) {
|
||||||
|
throw new Error(`API request failed with status: ${proxyResponse[1]}`);
|
||||||
|
}
|
||||||
|
const response = JSON.parse(proxyResponse[0]);
|
||||||
|
if (!response || !response.success || !response.jwt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return response.jwt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远程许可证验证
|
||||||
|
*/
|
||||||
|
static async checkLicenseRemote(): Promise<LicenseCheckResult> {
|
||||||
|
try {
|
||||||
|
const domain = this.getDomain();
|
||||||
|
const pluginId = this.getPluginId();
|
||||||
|
|
||||||
|
const jwt = await this.callRemoteLicenseAPI(domain, pluginId);
|
||||||
|
|
||||||
|
if (!jwt) {
|
||||||
|
return {
|
||||||
|
isLicenseValid: false,
|
||||||
|
isRemote: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存 JWT 到本地存储,获取保存的许可证结构
|
||||||
|
const savedLicense = LicenseStorage.saveLicense(jwt);
|
||||||
|
if (!savedLicense) {
|
||||||
|
return {
|
||||||
|
isLicenseValid: false,
|
||||||
|
isRemote: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用解析后的JWT进行验证
|
||||||
|
const isValid = this.checkLicenseAvailable(savedLicense);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLicenseValid: isValid,
|
||||||
|
license: savedLicense.licenseInfo,
|
||||||
|
isRemote: true,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error($t('license.error.fetchFailed', { e: error }));
|
||||||
|
return {
|
||||||
|
isLicenseValid: false,
|
||||||
|
isRemote: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 数据处理函数 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否快要到期
|
||||||
|
*/
|
||||||
|
static isExpiringSoon(expiryTimestamp: number): boolean {
|
||||||
|
const now = Date.now();
|
||||||
|
const warningTime = expiryTimestamp - config.license.warningDaysBeforeExpiry * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
return now >= warningTime && expiryTimestamp > now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化到期时间
|
||||||
|
*/
|
||||||
|
static formatExpiryDate(timestamp: number): string {
|
||||||
|
if (timestamp === -1) return $t('license.status.permanent');
|
||||||
|
return new Date(timestamp).toLocaleDateString(kintone.getLoginUser().language);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算剩余天数
|
||||||
|
*/
|
||||||
|
static getDaysRemaining(timestamp: number): number {
|
||||||
|
if (timestamp === -1) return Infinity;
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const remainingMs = timestamp - now;
|
||||||
|
return Math.floor(remainingMs / (24 * 60 * 60 * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ UI显示函数 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示到期警告通知
|
||||||
|
*/
|
||||||
|
static async showNotification(license: LicenseInfo | undefined, isWarning: boolean) {
|
||||||
|
let message;
|
||||||
|
const plugin = this.getPluginName();
|
||||||
|
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, days });
|
||||||
|
} else {
|
||||||
|
// 既に期限切れ
|
||||||
|
message = $t('license.notification.expired', { plugin });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await kintone.isMobilePage()) {
|
||||||
|
const notification = new MobileNotification({
|
||||||
|
content: message,
|
||||||
|
});
|
||||||
|
notification.open();
|
||||||
|
} else {
|
||||||
|
const link = `https://alicorn.cybozu.com/k/admin/app/${kintone.app.getId()}/plugin/config?pluginId=${this.getPluginId()}`;
|
||||||
|
const notification = new Notification({
|
||||||
|
content: message + '<br />' + $t('license.notification.gotoLink', { link }),
|
||||||
|
type: isWarning ? 'info' : 'danger',
|
||||||
|
});
|
||||||
|
notification.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 主要入口函数 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查插件功能访问权限并加载插件(如果获得授权)
|
||||||
|
*/
|
||||||
|
static async loadPluginIfAuthorized(pluginId: string, callback: () => void | Promise<void>) {
|
||||||
|
this.PLUGIN_ID = pluginId;
|
||||||
|
try {
|
||||||
|
if (!config.license.enabled) {
|
||||||
|
// 许可证功能未启用,直接加载插件
|
||||||
|
await callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查许可证(内部已经包含本地检查无效时自动获取远程的逻辑)
|
||||||
|
const licenseCheck = await this.checkLicense();
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
const permissions = await PermissionService.checkPermissions();
|
||||||
|
const isManager = permissions.canManagePlugins;
|
||||||
|
|
||||||
|
// 许可证无效的情况,直接返回不加载
|
||||||
|
if (!licenseCheck.isLicenseValid) {
|
||||||
|
if (!isManager) {
|
||||||
|
// 普通用户静默输出
|
||||||
|
console.warn($t('license.status.expiredDisplay'));
|
||||||
|
} else {
|
||||||
|
// 管理员可以看到过期弹框
|
||||||
|
this.showNotification(licenseCheck.license, false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 许可证有效,如果快要到期,管理员可以看到警告
|
||||||
|
if (
|
||||||
|
isManager &&
|
||||||
|
licenseCheck.license &&
|
||||||
|
!licenseCheck.license.isPaid &&
|
||||||
|
this.isExpiringSoon(licenseCheck.license.expiredTime)
|
||||||
|
) {
|
||||||
|
// 管理员可以看到过期弹框
|
||||||
|
this.showNotification(licenseCheck.license, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 许可证有效,可以加载插件功能
|
||||||
|
await callback();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn($t('license.error.checkFailed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 许可证信息管理 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取许可证信息(用于显示)
|
||||||
|
*/
|
||||||
|
static getLocalLicenseInfo(): LicenseInfo | null {
|
||||||
|
const savedLicense = LicenseStorage.getLicense(this.getPluginId());
|
||||||
|
if (!savedLicense) return null;
|
||||||
|
|
||||||
|
return savedLicense.licenseInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制刷新许可证(清除本地缓存重新检查)
|
||||||
|
*/
|
||||||
|
static async forceRefreshLicense(): Promise<LicenseCheckResult> {
|
||||||
|
// 清除本地缓存
|
||||||
|
LicenseStorage.clearLicense(this.getPluginId());
|
||||||
|
return await this.checkLicenseRemote();
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/types/index.d.ts
vendored
1
src/types/index.d.ts
vendored
@@ -1,3 +1,4 @@
|
|||||||
// 导出所有类型定义
|
// 导出所有类型定义
|
||||||
// 主要导出应用设置相关的类型接口
|
// 主要导出应用设置相关的类型接口
|
||||||
export * from './model';
|
export * from './model';
|
||||||
|
export * from './license';
|
||||||
|
|||||||
48
src/types/license.d.ts
vendored
Normal file
48
src/types/license.d.ts
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { KJUR } from 'jsrsasign';
|
||||||
|
// 许可证相关类型定义
|
||||||
|
|
||||||
|
export interface LicenseInfo {
|
||||||
|
// 到期时间戳,后端需要是最后一天的 23:59:59 如果是永久许可证则为 -1
|
||||||
|
expiredTime: number;
|
||||||
|
// 是否是永久许可证
|
||||||
|
isPaid: boolean;
|
||||||
|
// 域名
|
||||||
|
domain: string;
|
||||||
|
// 插件ID
|
||||||
|
pluginId: string;
|
||||||
|
// 获取许可证的时间戳
|
||||||
|
fetchTime: number;
|
||||||
|
// 版本号
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LicenseSetting {
|
||||||
|
suppressMsgTime?: number;
|
||||||
|
hideLicenseAreaIfPaid?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LicenseCheckResult {
|
||||||
|
// 是否有效(许可证真正有效可用)
|
||||||
|
isLicenseValid: boolean;
|
||||||
|
// 许可证信息
|
||||||
|
license?: LicenseInfo;
|
||||||
|
// 是否来自远程检查(从 checkLicenseRemote 返回时为 true)
|
||||||
|
isRemote?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PermissionConfig {
|
||||||
|
// 是否可以管理插件(应用管理员权限)
|
||||||
|
canManagePlugins: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扩展 JWSResult,将 payloadObj 类型限定为 LicenseInfo
|
||||||
|
export interface LicenseJWSResult extends Omit<KJUR.jws.JWS.JWSResult, 'payloadObj'> {
|
||||||
|
payloadObj?: LicenseInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT 解码后的结构
|
||||||
|
export interface SavedLicense {
|
||||||
|
jwt: string;
|
||||||
|
parsed: LicenseJWSResult;
|
||||||
|
licenseInfo: LicenseInfo;
|
||||||
|
}
|
||||||
10
src/types/my-kintone.d.ts
vendored
10
src/types/my-kintone.d.ts
vendored
@@ -1,6 +1,16 @@
|
|||||||
// 导入官方 REST API 客户端的类型定义
|
// 导入官方 REST API 客户端的类型定义
|
||||||
import { KintoneFormFieldProperty } from '@kintone/rest-api-client';
|
import { KintoneFormFieldProperty } from '@kintone/rest-api-client';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace kintone {
|
||||||
|
/**
|
||||||
|
* 判断当前页面是否为移动端页面
|
||||||
|
* @returns Promise<boolean> 返回是否为移动端页面
|
||||||
|
*/
|
||||||
|
function isMobilePage(): Promise<boolean>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// KUC 组件事件类型定义
|
// KUC 组件事件类型定义
|
||||||
// 定义 kintone UI 组件触发事件的结构
|
// 定义 kintone UI 组件触发事件的结构
|
||||||
export interface KucEvent<T> {
|
export interface KucEvent<T> {
|
||||||
|
|||||||
147
src/utils/license-storage.ts
Normal file
147
src/utils/license-storage.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// 本地存储加密工具类
|
||||||
|
import { LicenseService } from '@/services/license-service';
|
||||||
|
import type { LicenseJWSResult, LicenseSetting, SavedLicense } from '@/types/license';
|
||||||
|
import { KJUR } from 'jsrsasign';
|
||||||
|
|
||||||
|
export class LicenseStorage {
|
||||||
|
private static readonly STORAGE_KEY_PREFIX = 'alicorns_plugin_';
|
||||||
|
private static readonly STORAGE_SETTING_KEY_PREFIX = this.STORAGE_KEY_PREFIX + 'setting_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成存储key
|
||||||
|
*/
|
||||||
|
private static generateStorageKey(pluginId: string): string {
|
||||||
|
return `${this.STORAGE_KEY_PREFIX}${pluginId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 JWT 到本地存储
|
||||||
|
*/
|
||||||
|
static saveLicense(jwt: string): SavedLicense | null {
|
||||||
|
try {
|
||||||
|
// 从 JWT 中提取 pluginId 以生成存储key
|
||||||
|
if (!jwt) return null;
|
||||||
|
|
||||||
|
// 解码JWT
|
||||||
|
const savedLicense = this.convertToSavedLicense(jwt);
|
||||||
|
if (!savedLicense) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = this.generateStorageKey(savedLicense.licenseInfo.pluginId);
|
||||||
|
localStorage.setItem(key, jwt);
|
||||||
|
|
||||||
|
return savedLicense;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save license:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本地存储获取许可证信息
|
||||||
|
*/
|
||||||
|
static getLicense(pluginId: string): SavedLicense | null {
|
||||||
|
try {
|
||||||
|
const key = this.generateStorageKey(pluginId);
|
||||||
|
const storedJWT = localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (!storedJWT) return null;
|
||||||
|
|
||||||
|
// 解码JWT
|
||||||
|
const savedLicense = this.convertToSavedLicense(storedJWT);
|
||||||
|
if (!savedLicense) {
|
||||||
|
// JWT解析失败,清理存储
|
||||||
|
this.clearLicense(pluginId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = LicenseService.checkLicenseAvailable(savedLicense);
|
||||||
|
if (!isValid) {
|
||||||
|
// 获取许可证信息失败,清理存储
|
||||||
|
this.clearLicense(pluginId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return savedLicense;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get license:', error);
|
||||||
|
// 如果获取失败,清空本地存储
|
||||||
|
this.clearLicense(pluginId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static convertToSavedLicense(storedJWT: string): SavedLicense | null {
|
||||||
|
if (!storedJWT) return null;
|
||||||
|
// decode
|
||||||
|
let decodedJWT: LicenseJWSResult;
|
||||||
|
try {
|
||||||
|
decodedJWT = KJUR.jws.JWS.parse(storedJWT) as LicenseJWSResult;
|
||||||
|
if (!decodedJWT) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get license:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
jwt: storedJWT,
|
||||||
|
parsed: decodedJWT,
|
||||||
|
licenseInfo: decodedJWT.payloadObj!,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除许可证信息
|
||||||
|
*/
|
||||||
|
static clearLicense(pluginId: string): void {
|
||||||
|
const key = this.generateStorageKey(pluginId);
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成存储key
|
||||||
|
*/
|
||||||
|
private static generateSettingStorageKey(pluginId: string): string {
|
||||||
|
return `${this.STORAGE_SETTING_KEY_PREFIX}${pluginId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存设置信息到本地存储
|
||||||
|
*/
|
||||||
|
static saveSetting(setting: LicenseSetting, pluginId: string): void {
|
||||||
|
try {
|
||||||
|
const key = this.generateSettingStorageKey(pluginId);
|
||||||
|
localStorage.setItem(key, JSON.stringify(setting));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save setting:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本地存储获取设置信息
|
||||||
|
*/
|
||||||
|
static getSettings(pluginId: string): LicenseSetting {
|
||||||
|
try {
|
||||||
|
const key = this.generateSettingStorageKey(pluginId);
|
||||||
|
const storedData = localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (!storedData) return {};
|
||||||
|
|
||||||
|
return JSON.parse(storedData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get setting:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除设置信息
|
||||||
|
*/
|
||||||
|
static clearSetting(pluginId: string): void {
|
||||||
|
const key = this.generateSettingStorageKey(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() || kintone.mobile.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