feat: 改用bun进行完整实现
This commit is contained in:
parent
be05643c03
commit
c1a4890eec
@ -19,10 +19,10 @@
|
||||
"litiany4.umijs-plugin-model",
|
||||
"oderwat.indent-rainbow",
|
||||
"jock.svg",
|
||||
"aminer.codegeex",
|
||||
"ChakrounAnas.turbo-console-log",
|
||||
"Gruntfuggly.todo-tree",
|
||||
"MS-CEINTL.vscode-language-pack-zh-hans"
|
||||
"MS-CEINTL.vscode-language-pack-zh-hans",
|
||||
"Alibaba-Cloud.tongyi-lingma"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
32
app.js
32
app.js
@ -1,32 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const AutoLoad = require('@fastify/autoload')
|
||||
const { initSchedule } = require('./schedule')
|
||||
|
||||
// Init Scheduler
|
||||
initSchedule()
|
||||
|
||||
// Pass --options via CLI arguments in command to enable these options.
|
||||
module.exports.options = {}
|
||||
|
||||
module.exports = async function (fastify, opts) {
|
||||
// Place here your custom code!
|
||||
|
||||
// Do not touch the following lines
|
||||
|
||||
// This loads all plugins defined in plugins
|
||||
// those should be support plugins that are reused
|
||||
// through your application
|
||||
fastify.register(AutoLoad, {
|
||||
dir: path.join(__dirname, 'plugins'),
|
||||
options: Object.assign({}, opts)
|
||||
})
|
||||
|
||||
// This loads all plugins defined in routes
|
||||
// define your routes in one of these
|
||||
fastify.register(AutoLoad, {
|
||||
dir: path.join(__dirname, 'routes'),
|
||||
options: Object.assign({}, opts)
|
||||
})
|
||||
}
|
13
index.ts
Normal file
13
index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { manageBotReq } from "./routes/bot";
|
||||
|
||||
Bun.serve({
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
// 根路由
|
||||
if (url.pathname === "/") return new Response("hello, glade to see you!");
|
||||
// 机器人
|
||||
if (url.pathname === '/bot') return await manageBotReq(req);
|
||||
return Response.json({a: 'b'});
|
||||
},
|
||||
port: 3000
|
||||
});
|
34
package.json
34
package.json
@ -1,27 +1,17 @@
|
||||
{
|
||||
"name": "fastify",
|
||||
"version": "1.0.0",
|
||||
"description": "This project was bootstrapped with Fastify-CLI.",
|
||||
"main": "app.js",
|
||||
"name": "egg_server",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "fastify start -l info app.js",
|
||||
"dev": "fastify start -w -l info -P app.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fastify/autoload": "^5.0.0",
|
||||
"@fastify/sensible": "^5.0.0",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"fastify": "^4.0.0",
|
||||
"fastify-cli": "^5.8.0",
|
||||
"fastify-plugin": "^4.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"node-schedule": "^2.1.1",
|
||||
"pocketbase": "^0.16.0"
|
||||
"start": "bun run index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^16.1.0"
|
||||
"bun-types": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"pocketbase": "^0.21.1"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
# Plugins Folder
|
||||
|
||||
Plugins define behavior that is common to all the routes in your
|
||||
application. Authentication, caching, templates, and all the other cross
|
||||
cutting concerns should be handled by plugins placed in this folder.
|
||||
|
||||
Files in this folder are typically defined through the
|
||||
[`fastify-plugin`](https://github.com/fastify/fastify-plugin) module,
|
||||
making them non-encapsulated. They can define decorators and set hooks
|
||||
that will then be used in the rest of your application.
|
||||
|
||||
Check out:
|
||||
|
||||
* [The hitchhiker's guide to plugins](https://www.fastify.io/docs/latest/Guides/Plugins-Guide/)
|
||||
* [Fastify decorators](https://www.fastify.io/docs/latest/Reference/Decorators/).
|
||||
* [Fastify lifecycle](https://www.fastify.io/docs/latest/Reference/Lifecycle/).
|
@ -1,14 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const fp = require('fastify-plugin')
|
||||
|
||||
/**
|
||||
* This plugins adds some utilities to handle http errors
|
||||
*
|
||||
* @see https://github.com/fastify/fastify-sensible
|
||||
*/
|
||||
module.exports = fp(async function (fastify, opts) {
|
||||
fastify.register(require('@fastify/sensible'), {
|
||||
errorHandler: false
|
||||
})
|
||||
})
|
@ -1,12 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const fp = require('fastify-plugin')
|
||||
|
||||
// the use of fastify-plugin is required to be able
|
||||
// to export the decorators to the outer scope
|
||||
|
||||
module.exports = fp(async function (fastify, opts) {
|
||||
fastify.decorate('someSupport', function () {
|
||||
return 'hugs'
|
||||
})
|
||||
})
|
@ -1,138 +0,0 @@
|
||||
const { isSingleRemind } = require("../../utils");
|
||||
const { genSetRemindCard, genRemindCard } = require("../../utils/genCard");
|
||||
const { getNextRemindTime } = require("../../utils/nextRemindTime");
|
||||
const {
|
||||
upsertUser,
|
||||
upsertRemindByMessageId,
|
||||
getRemindRecordByMessageId,
|
||||
getRemind,
|
||||
updateRemind,
|
||||
updateRemindRecord,
|
||||
} = require("../../utils/pb");
|
||||
const { updateCard } = require("../../utils/sendMsg");
|
||||
const { trans2LocalTime, getNowStr, getDelayedTimeStr } = require("../../utils/time");
|
||||
|
||||
/**
|
||||
* 是否为Action消息
|
||||
* @param {LarkUserAction} body
|
||||
*/
|
||||
module.exports.isActionMsg = (body) => {
|
||||
return body?.action;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取Action类型
|
||||
* @param {LarkUserAction} body
|
||||
* @returns {string} Action类型
|
||||
*/
|
||||
const getActionType = (body) => {
|
||||
return body?.action?.tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理时间选择事件
|
||||
* @param {LarkUserAction} body
|
||||
* @returns
|
||||
*/
|
||||
const manageTimePicker = async (body) => {
|
||||
const userInfo = await upsertUser(body);
|
||||
const { content } = body?.action?.value;
|
||||
if (!content) return;
|
||||
const rawTime = body?.action?.option;
|
||||
// 将'2023-09-03 10:35 +0700'转换为'2023-09-03 11:35'
|
||||
const time = trans2LocalTime(rawTime);
|
||||
// 判断选择的时间是否是将来的时间
|
||||
if (new Date(time) <= new Date()) {
|
||||
const card = genSetRemindCard(
|
||||
"pendingErr",
|
||||
content,
|
||||
getNowStr(),
|
||||
userInfo.userId
|
||||
);
|
||||
await updateCard(body.open_message_id, card);
|
||||
return;
|
||||
}
|
||||
const remindInfo = {
|
||||
owner: userInfo.id,
|
||||
messageId: body.open_message_id,
|
||||
subscriberType: "chat_id",
|
||||
subscriberId: body.open_chat_id,
|
||||
needReply: false,
|
||||
delayTime: 0,
|
||||
cardInfo: {
|
||||
title: "🍳小煎蛋提醒!",
|
||||
content: `📝 ${content}`,
|
||||
},
|
||||
remindTimes: [{
|
||||
frequency: "single",
|
||||
time,
|
||||
}],
|
||||
enabled: true,
|
||||
nextRemindTime: time,
|
||||
nextRemindTimeCHS: time,
|
||||
};
|
||||
// 数据库写入提醒,这里根据messageId更新或者创建是为了防止垃圾飞书不更新消息
|
||||
await upsertRemindByMessageId(remindInfo);
|
||||
// 更新卡片
|
||||
const card = genSetRemindCard("confirmed", content, time, userInfo.userId);
|
||||
await updateCard(body.open_message_id, card);
|
||||
return;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理按钮点击事件
|
||||
* @param {LarkUserAction} body
|
||||
*/
|
||||
const manageBtnClick = async (body) => {
|
||||
const { result, type, text, remindId } = body?.action?.value;
|
||||
if (!type) return;
|
||||
// 当前点击的卡片
|
||||
const remindRecord = await getRemindRecordByMessageId(body.open_message_id);
|
||||
// 没有卡片,返回
|
||||
if (!remindRecord) return;
|
||||
// 当前卡片的提醒信息
|
||||
const remind = await getRemind(remindId)
|
||||
// 没有提醒信息,返回
|
||||
if (!remind) return;
|
||||
// 当前时间即为回复时间
|
||||
const interactTime = getNowStr()
|
||||
// 创建卡片
|
||||
const card = genRemindCard(remind, type, { type, text }, interactTime)
|
||||
// 更新飞书的卡片
|
||||
await updateCard(body.open_message_id, card)
|
||||
// 更新remindRecord
|
||||
const newRecord = {
|
||||
status: type,
|
||||
interactTime,
|
||||
result: { type, text, result },
|
||||
}
|
||||
await updateRemindRecord(remindRecord.id, newRecord)
|
||||
|
||||
// 如果是非延迟的单次提醒到此就结束了
|
||||
if (type !== 'delayed' && isSingleRemind(remind.remindTimes)) {
|
||||
updateRemind(remind.id, { enabled: false })
|
||||
return
|
||||
}
|
||||
// 延迟提醒
|
||||
if (type === 'delayed') {
|
||||
updateRemind(remind.id, { nextRemindTime: getDelayedTimeStr(remind.delayTime) })
|
||||
return
|
||||
}
|
||||
// 更新下一次的提醒时间
|
||||
updateRemind(remind.id, getNextRemindTime(remind.remindTimes))
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理Action消息
|
||||
* @param {LarkUserAction} body
|
||||
*/
|
||||
module.exports.manageActionMsg = async (body) => {
|
||||
const actionType = getActionType(body);
|
||||
if (actionType === 'picker_datetime') {
|
||||
manageTimePicker(body);
|
||||
}
|
||||
if (actionType === 'button') {
|
||||
manageBtnClick(body);
|
||||
}
|
||||
return;
|
||||
};
|
@ -1,115 +0,0 @@
|
||||
const { genSetRemindCard } = require("../../utils/genCard");
|
||||
const { sendMsg } = require("../../utils/sendMsg");
|
||||
const { getDelayedTimeStr } = require("../../utils/time");
|
||||
|
||||
/**
|
||||
* 获取事件文本类型
|
||||
* @param {LarkMessageEvent} body
|
||||
* @returns
|
||||
*/
|
||||
const getMsgType = (body) => {
|
||||
return body?.event?.message?.message_type
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话流Id
|
||||
* @param {LarkMessageEvent} body
|
||||
* @returns
|
||||
*/
|
||||
const getChatId = (body) => {
|
||||
return body?.event?.message?.chat_id
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文本内容并剔除艾特信息
|
||||
* @param {LarkMessageEvent} body
|
||||
* @returns {string} 文本内容
|
||||
*/
|
||||
const getMsgText = (body) => {
|
||||
// TODO: 如果之后想支持单独提醒,这里需要做模板解析
|
||||
try {
|
||||
const { text } = JSON.parse(body?.event?.message?.content)
|
||||
// 去掉@_user_1相关的内容,例如 '@_user_1 测试' -> '测试'
|
||||
const textWithoutAt = text.replace(/@_user_\d+/g, '')
|
||||
// 去除空格和换行
|
||||
const textWithoutSpace = textWithoutAt.replace(/[\s\n]/g, '')
|
||||
return textWithoutSpace
|
||||
}
|
||||
catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤出非法消息,如果发表情包就直接发回去
|
||||
* @param {LarkMessageEvent} body
|
||||
* @returns {boolean} 是否为非法消息
|
||||
*/
|
||||
const filterIllegalMsg = (body) => {
|
||||
const chatId = getChatId(body)
|
||||
if (!chatId) return true
|
||||
const msgType = getMsgType(body)
|
||||
if (msgType === 'sticker') {
|
||||
const content = body?.event?.message?.content
|
||||
sendMsg('chat_id', chatId, 'sticker', content)
|
||||
return true
|
||||
}
|
||||
if (msgType !== 'text') {
|
||||
const textList = [
|
||||
'仅支持普通文本内容[黑脸]',
|
||||
'唔...我只能处理普通文本哦[泣不成声]',
|
||||
'噢!这似乎是个非普通文本[看]',
|
||||
'哇!这是什么东东?我只懂普通文本啦![可爱]',
|
||||
'只能处理普通文本内容哦[捂脸]',
|
||||
]
|
||||
const content = JSON.stringify({ text: textList[Math.floor(Math.random() * textList.length)] })
|
||||
sendMsg('chat_id', chatId, 'text', content)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤出info指令
|
||||
* @param {LarkMessageEvent} body
|
||||
* @returns {boolean} 是否为info指令
|
||||
*/
|
||||
const filterGetInfoCommand = (body) => {
|
||||
const chatId = getChatId(body)
|
||||
const text = getMsgText(body)
|
||||
if (text !== 'info') return false
|
||||
const content = JSON.stringify({ text: JSON.stringify(body)})
|
||||
sendMsg('chat_id', chatId, 'text', content)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送设置提醒卡片
|
||||
* @param {LarkMessageEvent} body
|
||||
*/
|
||||
const sendSetRemindCard = async (body) => {
|
||||
const text = getMsgText(body);
|
||||
const setRemindCard = genSetRemindCard("pending", text, getDelayedTimeStr(1));
|
||||
await sendMsg("chat_id", getChatId(body), "interactive", setRemindCard);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理消息
|
||||
* @param {LarkMessageEvent} body
|
||||
*/
|
||||
module.exports.manageEventMsg = async (body) => {
|
||||
// 过滤非法消息
|
||||
if (filterIllegalMsg(body)) return "OK";
|
||||
// 过滤获取用户信息的指令
|
||||
if (filterGetInfoCommand(body)) return "OK";
|
||||
// 发送设置提醒卡片
|
||||
sendSetRemindCard(body);
|
||||
};
|
||||
|
||||
/**
|
||||
* 是否为事件消息
|
||||
* @param {LarkMessageEvent} body
|
||||
*/
|
||||
module.exports.isEventMsg = (body) => {
|
||||
return body?.header?.event_type === "im.message.receive_v1";
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const { isEventMsg, manageEventMsg } = require("./eventMsg");
|
||||
const { isActionMsg, manageActionMsg } = require("./actionMsg");
|
||||
|
||||
module.exports = async function (fastify, opts) {
|
||||
// 机器人验证及分发
|
||||
fastify.post("/", async function (request, reply) {
|
||||
console.log(JSON.stringify(request.body));
|
||||
// 验证机器人
|
||||
if (request.body.type === "url_verification") {
|
||||
console.log("url_verification");
|
||||
return { challenge: request.body.challenge };
|
||||
}
|
||||
if (isEventMsg(request.body)) {
|
||||
// 处理事件消息
|
||||
manageEventMsg(request.body);
|
||||
}
|
||||
if (isActionMsg(request.body)) {
|
||||
// 处理Action消息
|
||||
manageActionMsg(request.body);
|
||||
}
|
||||
reply.send('OK')
|
||||
});
|
||||
};
|
9
routes/bot/index.ts
Normal file
9
routes/bot/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const manageBotReq = async (req: Request) => {
|
||||
const body = await req.json() as any
|
||||
// 验证机器人
|
||||
if (body?.type === 'url_verification') {
|
||||
console.log("🚀 ~ manageBotReq ~ url_verification:")
|
||||
return Response.json({ challenge: body?.challenge })
|
||||
}
|
||||
return new Response("hello, glade to see you!")
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = async function (fastify, opts) {
|
||||
fastify.get('/', async function (request, reply) {
|
||||
return 'hello, glade to see you!'
|
||||
})
|
||||
}
|
1
run.sh
1
run.sh
@ -1 +0,0 @@
|
||||
docker run -it --rm -v $(pwd):/app -w /app -p 3000:3000 micr.cloud.mioffice.cn/zhaoyingbo/dev:18.14.2 bash
|
@ -1,21 +0,0 @@
|
||||
const fetch = require('node-fetch');
|
||||
const { updateTenantAccessToken } = require('../utils/pb');
|
||||
|
||||
exports.resetAccessToken = async () => {
|
||||
const URL = 'https://open.f.mioffice.cn/open-apis/auth/v3/tenant_access_token/internal'
|
||||
const app_id = 'cli_a1eff35b43b89063'
|
||||
const app_secret = 'IFSl8ig5DMwMnFjwPiljCfoEWlgRwDxW'
|
||||
const res = await fetch(URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
app_id,
|
||||
app_secret
|
||||
})
|
||||
})
|
||||
const { tenant_access_token } = await res.json()
|
||||
await updateTenantAccessToken(tenant_access_token)
|
||||
return tenant_access_token
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
|
||||
const schedule = require('node-schedule');
|
||||
const { resetAccessToken } = require('./accessToken');
|
||||
const { sendCurrTimeReminds } = require('../schedule/remind');
|
||||
|
||||
exports.initSchedule = async () => {
|
||||
// 定时任务,每15分钟刷新一次token
|
||||
schedule.scheduleJob('*/15 * * * *', resetAccessToken);
|
||||
// 定时任务,每分钟检查一次是否有需要提醒的卡片
|
||||
schedule.scheduleJob('* * * * *', sendCurrTimeReminds);
|
||||
// 立即执行一次
|
||||
resetAccessToken()
|
||||
sendCurrTimeReminds()
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
const { isSingleRemind } = require("../utils")
|
||||
const { genRemindCard } = require("../utils/genCard")
|
||||
const { getNextRemindTime } = require("../utils/nextRemindTime")
|
||||
const { getCurrTimeRemind, updateRemind, getPendingRemindRecord, updateRemindRecord, createRemindRecord } = require("../utils/pb")
|
||||
const { sendMsg, updateCard } = require("../utils/sendMsg")
|
||||
const { getNowStr, getDelayedTimeStr } = require("../utils/time")
|
||||
|
||||
/**
|
||||
* 更新上一个pending状态的卡片至delayed
|
||||
*/
|
||||
const updateLastPendingCardToDelayed = async (remind) => {
|
||||
const record = await getPendingRemindRecord(remind.id)
|
||||
if (!record) return
|
||||
// 当前时间即为回复时间
|
||||
const interactTime = getNowStr()
|
||||
// 创建delayed状态的卡片
|
||||
const card = genRemindCard(remind, 'delayed', null, interactTime)
|
||||
// 更新飞书的卡片
|
||||
await updateCard(record.messageId, card)
|
||||
// 更新remindRecord
|
||||
const newRecord = {
|
||||
...record,
|
||||
status: 'delayed',
|
||||
interactTime,
|
||||
}
|
||||
await updateRemindRecord(record.id, newRecord)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理提醒。包括:创建卡片、更新下一次提醒时间,更新上一个pending状态的卡片至delayed
|
||||
* @param {Remind} remind 提醒信息
|
||||
*/
|
||||
const manageRemind = async (remind) => {
|
||||
// 更新上一个pending状态的卡片至delayed
|
||||
await updateLastPendingCardToDelayed(remind)
|
||||
const cardType = remind.needReply ? 'pending' : 'confirmed'
|
||||
// 生成卡片
|
||||
const card = genRemindCard(remind, cardType, null, null)
|
||||
// 发送卡片
|
||||
const messageId = await sendMsg(remind.subscriberType, remind.subscriberId, 'interactive', card)
|
||||
// 创建remindRecord
|
||||
await createRemindRecord(remind.id, messageId, cardType)
|
||||
// 如果是不需要回复的单次提醒到此就结束了
|
||||
if (isSingleRemind(remind.remindTimes) && !remind.needReply) {
|
||||
updateRemind(remind.id, { enabled: false })
|
||||
return
|
||||
}
|
||||
// 需要回复的卡片下次提醒时间是delayTime(min)之后的时间
|
||||
if (remind.needReply) {
|
||||
updateRemind(remind.id, { nextRemindTime: getDelayedTimeStr(remind.delayTime) })
|
||||
return
|
||||
}
|
||||
// 不需要回复的循环提醒,更新下一次的提醒时间
|
||||
updateRemind(remind.id, getNextRemindTime(remind.remindTimes))
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理当前分钟需要处理的卡片
|
||||
* 发送提醒、更新上一个pending状态的卡片至delayed
|
||||
*/
|
||||
module.exports.sendCurrTimeReminds = async () => {
|
||||
const remindList = await getCurrTimeRemind()
|
||||
// 没有需要提醒的卡片
|
||||
if (!remindList?.length) return
|
||||
// 处理提醒
|
||||
for (const remind of remindList) {
|
||||
manageRemind(remind)
|
||||
}
|
||||
}
|
5
test.js
5
test.js
@ -1,5 +0,0 @@
|
||||
const moment = require('moment')
|
||||
|
||||
const nowStr = moment().format('YYYY-MM-DD HH:mm')
|
||||
|
||||
console.log(nowStr)
|
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react-jsx",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": true,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
}
|
||||
}
|
441
typings.d.ts
vendored
441
typings.d.ts
vendored
@ -1,441 +0,0 @@
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
interface User {
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* 用户名
|
||||
* @example zhaoyingbo
|
||||
*/
|
||||
userId: string;
|
||||
/**
|
||||
* open_id
|
||||
*/
|
||||
openId: string;
|
||||
/**
|
||||
* 提醒列表
|
||||
*/
|
||||
remindList: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 提醒列表
|
||||
*/
|
||||
interface Remind {
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* 所有者信息,绑定用户表的id
|
||||
*/
|
||||
owner: string;
|
||||
/**
|
||||
* 消息Id
|
||||
*/
|
||||
messageId: string;
|
||||
/**
|
||||
* 接收者类型
|
||||
*/
|
||||
subscriberType: "open_id" | "user_id" | "union_id" | "email" | "chat_id";
|
||||
/**
|
||||
* 接收者Id
|
||||
*/
|
||||
subscriberId: string;
|
||||
/**
|
||||
* 是否需要回复,不需要回复的也不会重复提醒
|
||||
*/
|
||||
needReply: boolean;
|
||||
/**
|
||||
* 延迟时间
|
||||
*/
|
||||
delayTime: number;
|
||||
/**
|
||||
* 卡片信息,用于绘制初始卡片、确认卡片、取消卡片、延迟卡片
|
||||
*/
|
||||
cardInfo: {
|
||||
/**
|
||||
* 提醒标题,必须要有
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* 插图key
|
||||
*/
|
||||
imageKey?: string;
|
||||
/**
|
||||
* 提醒内容,为空不显示
|
||||
*/
|
||||
content?: string;
|
||||
/**
|
||||
* 确认文本,为空不显示,为需要回复卡片时,如果为空则默认为“完成”
|
||||
*/
|
||||
confirmText?: string;
|
||||
/**
|
||||
* 取消文本,为空不显示
|
||||
*/
|
||||
cancelText?: string;
|
||||
/**
|
||||
* 延迟文本,为空不显示
|
||||
*/
|
||||
delayText?: string;
|
||||
} | null;
|
||||
/**
|
||||
* 卡片模板信息
|
||||
*/
|
||||
templateInfo: {
|
||||
/**
|
||||
* 卡片模板ID,会注入变量
|
||||
* ${owner} 所有者
|
||||
* ${remindTime} 提醒时间
|
||||
*/
|
||||
pendingTemplateId: string;
|
||||
/**
|
||||
* 交互之后的卡片模板ID,如果有这个就不会用下边三个但是都会注入变量
|
||||
* ${owner} 所有者
|
||||
* ${remindTime} 提醒时间
|
||||
* ${result} 交互结果,会读卡片按钮绑定的变量text,如果没有则是绑定的result对应的 已确认、已取消、已延迟
|
||||
* ${interactTime} 交互时间
|
||||
*/
|
||||
interactedTemplateId: string;
|
||||
/**
|
||||
* 确认之后的卡片模板ID
|
||||
*/
|
||||
confirmedTemplateId: string;
|
||||
/**
|
||||
* 取消之后的卡片模板ID
|
||||
*/
|
||||
cancelededTemplateId: string;
|
||||
/**
|
||||
* 延迟之后的卡片模板ID
|
||||
*/
|
||||
delayedTemplateId: string;
|
||||
} | null;
|
||||
|
||||
/**
|
||||
* 提醒时间
|
||||
*/
|
||||
remindTimes: RemindTime[];
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* 下次提醒的时间,格式为yyyy-MM-dd HH:mm
|
||||
*/
|
||||
nextRemindTime: string;
|
||||
/**
|
||||
* 下次提醒时间的中文
|
||||
*/
|
||||
nextRemindTimeCHS: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提醒时间
|
||||
* 为了支持多个时间点提醒,将时间存成数组
|
||||
*/
|
||||
interface RemindTime {
|
||||
/**
|
||||
* 重复类型
|
||||
* single: 一次性
|
||||
* daily: 每天
|
||||
* weekly: 每周
|
||||
* monthly: 每月
|
||||
* yearly: 每年
|
||||
* workday: 工作日
|
||||
* holiday: 节假日
|
||||
*/
|
||||
frequency:
|
||||
| "single"
|
||||
| "daily"
|
||||
| "weekly"
|
||||
| "monthly"
|
||||
| "yearly"
|
||||
| "workday"
|
||||
| "holiday";
|
||||
/**
|
||||
* 提醒时间,格式为HH:mm, single类型时仅作展示用,类型为yyyy-MM-dd HH:mm
|
||||
*/
|
||||
time: string;
|
||||
/**
|
||||
* 星期几[1-7],当frequency为weekly时有效
|
||||
*/
|
||||
daysOfWeek: number[];
|
||||
/**
|
||||
* 每月的几号[1-31],当frequency为monthly时有效
|
||||
*/
|
||||
daysOfMonth: number[];
|
||||
/**
|
||||
* 每年的哪天提醒,当frequency为 yearly 时有效,格式为MM-dd
|
||||
*/
|
||||
dayOfYear: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提醒记录
|
||||
* 记录提醒时间,回答结果等
|
||||
*/
|
||||
interface RemindRecord {
|
||||
/**
|
||||
* 记录Id
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* 关联的提醒Id
|
||||
*/
|
||||
remindId: string;
|
||||
/**
|
||||
* 发送的卡片Id
|
||||
*/
|
||||
messageId: string;
|
||||
/**
|
||||
* 提醒状态
|
||||
* pending: 待确认
|
||||
* delay: 已延迟
|
||||
* confirmed: 已确认
|
||||
* canceled: 已取消
|
||||
*/
|
||||
status: "pending" | "delayed" | "confirmed" | "canceled";
|
||||
/**
|
||||
* 本次提醒时间,格式为yyyy-MM-dd HH:mm
|
||||
*/
|
||||
remindTime: string;
|
||||
/**
|
||||
* 用户交互的时间,格式为yyyy-MM-dd HH:mm
|
||||
*/
|
||||
interactTime: string;
|
||||
/**
|
||||
* 用户回答的结果,类似每天 07:00
|
||||
*/
|
||||
result: object;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息事件头
|
||||
*/
|
||||
interface Header {
|
||||
/**
|
||||
* 事件ID
|
||||
* @example 0f8ab23b60993cf8dd15c8cde4d7b0f5
|
||||
*/
|
||||
event_id: string;
|
||||
/**
|
||||
* token
|
||||
* @example tV9djUKSjzVnekV7xTg2Od06NFTcsBnj
|
||||
*/
|
||||
token: string;
|
||||
/**
|
||||
* 创建时间戳
|
||||
* @example 1693565712117
|
||||
*/
|
||||
create_time: string;
|
||||
/**
|
||||
* 事件类型
|
||||
* @example im.message.receive_v1
|
||||
*/
|
||||
event_type: string;
|
||||
/**
|
||||
* tenant_key
|
||||
* @example 2ee61fe50f4f1657
|
||||
*/
|
||||
tenant_key: string;
|
||||
/**
|
||||
* app_id
|
||||
* @example cli_a1eff35b43b89063
|
||||
*/
|
||||
app_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户ID信息
|
||||
*/
|
||||
interface UserIdInfo {
|
||||
/**
|
||||
* 用户标记
|
||||
* @example ou_032f507d08f9a7f28b042fcd086daef5
|
||||
*/
|
||||
open_id: string;
|
||||
/**
|
||||
* 用户标记
|
||||
* @example on_7111660fddd8302ce47bf1999147c011
|
||||
*/
|
||||
union_id: string;
|
||||
/**
|
||||
* 用户名
|
||||
* @example zhaoyingbo
|
||||
*/
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 被AT的人的信息
|
||||
*/
|
||||
interface Mention {
|
||||
/**
|
||||
* 被艾特的人的ID信息
|
||||
*/
|
||||
id: UserIdInfo;
|
||||
/**
|
||||
* 对应到文本内的内容
|
||||
* @example "@_user_1"
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* 用户名
|
||||
* @example 小煎蛋
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* 应用ID
|
||||
* @example 2ee61fe50f4f1657
|
||||
*/
|
||||
tenant_key: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息内容信息
|
||||
*/
|
||||
interface Message {
|
||||
/**
|
||||
* 对话流ID
|
||||
* @example oc_433b1cb7a9dbb7ebe70a4e1a59cb8bb1
|
||||
*/
|
||||
chat_id: string;
|
||||
/**
|
||||
* 消息类型
|
||||
* @example group | p2p
|
||||
*/
|
||||
chat_type: string;
|
||||
/**
|
||||
* JSON字符串文本内容
|
||||
* @example "{\"text\":\"@_user_1 测试\"}"
|
||||
*/
|
||||
content: string;
|
||||
/**
|
||||
* 消息发送时间戳
|
||||
* @example 1693565711996
|
||||
*/
|
||||
create_time: string;
|
||||
/**
|
||||
* 被艾特的人信息
|
||||
*/
|
||||
mentions?: Mention[];
|
||||
/**
|
||||
* 当前消息的ID
|
||||
* @example om_038fc0eceed6224a1abc1cdaa4266405
|
||||
*/
|
||||
message_id: string;
|
||||
/**
|
||||
* 消息类型
|
||||
* @example text、post、image、file、audio、media、sticker、interactive、share_chat、share_user
|
||||
*/
|
||||
message_type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息发送者信息
|
||||
*/
|
||||
interface Sender {
|
||||
/**
|
||||
* id 相关信息
|
||||
*/
|
||||
sender_id: UserIdInfo;
|
||||
/**
|
||||
* 发送者类型
|
||||
* @example user
|
||||
*/
|
||||
sender_type: string;
|
||||
/**
|
||||
* 应用ID
|
||||
* @example 2ee61fe50f4f1657
|
||||
*/
|
||||
tenant_key: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件详情
|
||||
*/
|
||||
interface Event {
|
||||
message: Message;
|
||||
sender: Sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件订阅信息
|
||||
*/
|
||||
interface LarkMessageEvent {
|
||||
/**
|
||||
* 协议版本
|
||||
* @example 2.0
|
||||
*/
|
||||
schema: string;
|
||||
/**
|
||||
* 事件头
|
||||
*/
|
||||
header: Header;
|
||||
/**
|
||||
* 事件详情
|
||||
*/
|
||||
event: Event;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户Action信息
|
||||
*/
|
||||
interface LarkUserAction {
|
||||
/**
|
||||
* open_id
|
||||
*/
|
||||
open_id: string;
|
||||
/**
|
||||
* 用户名
|
||||
* @example zhaoyingbo
|
||||
*/
|
||||
user_id: string;
|
||||
/**
|
||||
* 当前消息的ID
|
||||
* @example om_038fc0eceed6224a1abc1cdaa4266405
|
||||
*/
|
||||
open_message_id: string;
|
||||
/**
|
||||
* 对话流ID
|
||||
* @example oc_433b1cb7a9dbb7ebe70a4e1a59cb8bb1
|
||||
*/
|
||||
open_chat_id: string;
|
||||
/**
|
||||
* 应用ID
|
||||
* @example 2ee61fe50f4f1657
|
||||
*/
|
||||
tenant_key: string;
|
||||
/**
|
||||
* token
|
||||
* @example tV9djUKSjzVnekV7xTg2Od06NFTcsBnj
|
||||
*/
|
||||
token: string;
|
||||
/**
|
||||
* 事件结果
|
||||
*/
|
||||
action: {
|
||||
/**
|
||||
* 传的参数
|
||||
*/
|
||||
value: any;
|
||||
/**
|
||||
* 标签名
|
||||
* @example picker_datetime
|
||||
*/
|
||||
tag: string;
|
||||
/**
|
||||
* 选择的事件
|
||||
* @example 2023-09-03 10:35 +0800
|
||||
*/
|
||||
option: string;
|
||||
/**
|
||||
* 时区
|
||||
*/
|
||||
timezone: string;
|
||||
};
|
||||
}
|
375
utils/genCard.js
375
utils/genCard.js
@ -1,375 +0,0 @@
|
||||
/**
|
||||
* 生成交互结果
|
||||
*/
|
||||
const genResult = (interactInfo, cardInfo, cardType) => {
|
||||
// 优先卡片返回的信息
|
||||
if (interactInfo) {
|
||||
const { type, text } = interactInfo;
|
||||
if (text) return text;
|
||||
return {
|
||||
pending: "未交互",
|
||||
confirmed: "已确认",
|
||||
delayed: "已延期",
|
||||
canceled: "已取消",
|
||||
}[type];
|
||||
}
|
||||
if (!cardInfo) {
|
||||
return {
|
||||
pending: "未交互",
|
||||
confirmed: "已确认",
|
||||
delayed: "已延期",
|
||||
canceled: "已取消",
|
||||
}[cardType];
|
||||
}
|
||||
const { confirmText, delayText, cancelText } = cardInfo;
|
||||
return {
|
||||
pending: "未交互",
|
||||
confirmed: confirmText,
|
||||
delayed: delayText,
|
||||
canceled: cancelText,
|
||||
}[cardType];
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成交互按钮
|
||||
*/
|
||||
const genActions = (id, cardInfo, cardType, needReply) => {
|
||||
// 如果非交互提醒,不显示按钮
|
||||
if (!needReply) return null;
|
||||
// 如果是交互卡片的完成交互状态,不显示按钮
|
||||
if (cardType !== "pending") return null;
|
||||
|
||||
const { confirmText = "完成", cancelText, delayText } = cardInfo;
|
||||
const actions = [];
|
||||
const genBtn = (text, type) => ({
|
||||
tag: "button",
|
||||
text: {
|
||||
tag: "plain_text",
|
||||
content: text,
|
||||
},
|
||||
type: {
|
||||
confirmed: "primary",
|
||||
delayed: "default",
|
||||
canceled: "danger",
|
||||
}[type],
|
||||
value: {
|
||||
type,
|
||||
text,
|
||||
remindId: id,
|
||||
},
|
||||
});
|
||||
// 需要回复的卡片,显示确认、延期、取消按钮
|
||||
actions.push(genBtn(confirmText, "confirmed"));
|
||||
if (delayText) {
|
||||
actions.push(genBtn(delayText, "delayed"));
|
||||
}
|
||||
if (cancelText) {
|
||||
actions.push(genBtn(cancelText, "canceled"));
|
||||
}
|
||||
return {
|
||||
tag: "action",
|
||||
actions,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成提醒信息
|
||||
*/
|
||||
const genRemindInfo = (
|
||||
needReply,
|
||||
cardType,
|
||||
owner,
|
||||
nextRemindTimeCHS,
|
||||
interactTime,
|
||||
result
|
||||
) => {
|
||||
// 如果是交互卡片的待交互状态,不显示这个
|
||||
if (needReply && cardType === "pending") return null;
|
||||
const columns = [];
|
||||
const genColumn = (content) => ({
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
weight: 1,
|
||||
vertical_align: "top",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content,
|
||||
},
|
||||
],
|
||||
});
|
||||
// 如果是非交互卡片,展示创建人、提醒时间
|
||||
columns.push(genColumn(`**创建人**\n${owner}`));
|
||||
columns.push(genColumn(`**提醒时间**\n${nextRemindTimeCHS}`));
|
||||
// 如果是交互卡片,还需要展示交互结果、交互时间
|
||||
if (needReply) {
|
||||
columns.push(genColumn(`**交互结果**\n${result}`));
|
||||
columns.push(genColumn(`**交互时间**\n${interactTime}`));
|
||||
}
|
||||
return {
|
||||
tag: "column_set",
|
||||
flex_mode: "bisect",
|
||||
background_style: "grey",
|
||||
columns,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据提醒信息生成卡片
|
||||
* @param {Remind} remindInfo 提醒信息
|
||||
* @param {string} cardType 卡片类型
|
||||
* @param {object} interactInfo 交互信息
|
||||
* @param {string} interactTime 交互时间,在上层已经转成 yyyy-MM-dd HH:mm 格式了
|
||||
*/
|
||||
module.exports.genRemindCard = (
|
||||
remindInfo,
|
||||
cardType,
|
||||
interactInfo,
|
||||
interactTime
|
||||
) => {
|
||||
const {
|
||||
id,
|
||||
cardInfo,
|
||||
templateInfo,
|
||||
needReply,
|
||||
nextRemindTimeCHS,
|
||||
expand: {
|
||||
owner: {
|
||||
userId: owner,
|
||||
}
|
||||
},
|
||||
} = remindInfo;
|
||||
// 生成交互结果
|
||||
const result = genResult(interactInfo, cardInfo, cardType);
|
||||
// 优先返回模板信息
|
||||
if (templateInfo) {
|
||||
const templateVariable =
|
||||
cardType === "pending"
|
||||
? {
|
||||
owner,
|
||||
remindTime: nextRemindTimeCHS,
|
||||
}
|
||||
: {
|
||||
owner,
|
||||
remindTime: nextRemindTimeCHS,
|
||||
result,
|
||||
interactTime,
|
||||
};
|
||||
let templateId = templateInfo[`${cardType}TemplateId`];
|
||||
// 如果是非交互卡片且存在统一处理交互完成的卡片
|
||||
if (cardType !== "pending" && templateInfo.interactedTemplateId) {
|
||||
templateId = templateInfo.interactedTemplateId;
|
||||
}
|
||||
return {
|
||||
type: "template",
|
||||
data: {
|
||||
template_id: templateId,
|
||||
template_variable: templateVariable,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 其次卡片信息
|
||||
const { title, imageKey, content } = cardInfo;
|
||||
// 组织卡片内容,标题,pending: 蓝色、confirmed: 绿色、delayed: 黄色、canceled: 红色
|
||||
const header = {
|
||||
title: {
|
||||
content: title,
|
||||
tag: "plain_text",
|
||||
},
|
||||
template: {
|
||||
pending: "blue",
|
||||
confirmed: "green",
|
||||
delayed: "yellow",
|
||||
canceled: "red",
|
||||
}[cardType],
|
||||
};
|
||||
// 图片节点
|
||||
const imgDom = imageKey
|
||||
? {
|
||||
alt: {
|
||||
content: "",
|
||||
tag: "plain_text",
|
||||
},
|
||||
img_key: imageKey,
|
||||
tag: "img",
|
||||
}
|
||||
: null;
|
||||
// 内容节点
|
||||
const contentDom = content
|
||||
? {
|
||||
tag: "div",
|
||||
text: {
|
||||
content,
|
||||
tag: "lark_md",
|
||||
},
|
||||
}
|
||||
: null;
|
||||
// 待交互卡片且需要回复的显示按钮
|
||||
const actionsDom = genActions(id, cardInfo, cardType, needReply);
|
||||
// 非交互卡片以及交互完卡片,需要显示提醒信息
|
||||
const remindInfoDom = genRemindInfo(
|
||||
needReply,
|
||||
cardType,
|
||||
owner,
|
||||
nextRemindTimeCHS,
|
||||
interactTime,
|
||||
result
|
||||
);
|
||||
// 组织卡片内容
|
||||
const elements = [];
|
||||
if (imgDom) {
|
||||
elements.push(imgDom);
|
||||
}
|
||||
if (contentDom) {
|
||||
elements.push(contentDom);
|
||||
}
|
||||
if (actionsDom) {
|
||||
elements.push(actionsDom);
|
||||
}
|
||||
if (remindInfoDom) {
|
||||
elements.push(remindInfoDom);
|
||||
}
|
||||
return JSON.stringify({
|
||||
config: {
|
||||
enable_forward: true,
|
||||
update_multi: true,
|
||||
},
|
||||
elements,
|
||||
header,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成设置提醒卡片
|
||||
* @param {string} cardType 卡片类型
|
||||
* @param {*} content 卡片内容
|
||||
*/
|
||||
module.exports.genSetRemindCard = (cardType, content, time, userId) => {
|
||||
const action = {
|
||||
tag: "action",
|
||||
actions: [
|
||||
{
|
||||
tag: "picker_datetime",
|
||||
placeholder: {
|
||||
tag: "plain_text",
|
||||
content: "请选择提醒时间",
|
||||
},
|
||||
value: {
|
||||
content: `${content}`,
|
||||
},
|
||||
initial_datetime: time,
|
||||
},
|
||||
],
|
||||
layout: "bisected",
|
||||
};
|
||||
const tip = [
|
||||
{
|
||||
tag: "hr",
|
||||
},
|
||||
{
|
||||
tag: "note",
|
||||
elements: [
|
||||
{
|
||||
tag: "img",
|
||||
img_key: "img_v2_19db22c1-0030-434b-9b54-2a53b99c5f3l",
|
||||
alt: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: "plain_text",
|
||||
content: "谁选的时间提醒挂在谁名下呦",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const remindInfo = [
|
||||
{
|
||||
tag: "column_set",
|
||||
flex_mode: "none",
|
||||
background_style: "grey",
|
||||
columns: [
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
weight: 1,
|
||||
vertical_align: "top",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**创建人**\n${userId}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
weight: 1,
|
||||
vertical_align: "top",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**提醒时间**\n${time}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const errTip = {
|
||||
tag: "note",
|
||||
elements: [
|
||||
{
|
||||
tag: "plain_text",
|
||||
content: "🚫请选择将来的时间哦!",
|
||||
},
|
||||
],
|
||||
};
|
||||
const elements = [
|
||||
{
|
||||
tag: "div",
|
||||
text: {
|
||||
content: `📝 ${content}`,
|
||||
tag: "lark_md",
|
||||
},
|
||||
},
|
||||
];
|
||||
if (cardType === "pendingErr") {
|
||||
elements.push(action);
|
||||
elements.push(errTip);
|
||||
elements.push(...tip);
|
||||
}
|
||||
if (cardType === "pending") {
|
||||
elements.push(action);
|
||||
elements.push(...tip);
|
||||
}
|
||||
if (cardType === "confirmed") {
|
||||
elements.push(...remindInfo);
|
||||
}
|
||||
return JSON.stringify({
|
||||
config: {
|
||||
enable_forward: true,
|
||||
update_multi: true,
|
||||
},
|
||||
elements,
|
||||
header: {
|
||||
template: {
|
||||
pending: "turquoise",
|
||||
pendingErr: "turquoise",
|
||||
confirmed: "green",
|
||||
}[cardType],
|
||||
title: {
|
||||
content: `✨小煎蛋提醒创建${
|
||||
{
|
||||
pending: "工具",
|
||||
pendingErr: "工具",
|
||||
confirmed: "成功",
|
||||
}[cardType]
|
||||
}`,
|
||||
tag: "plain_text",
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
@ -1,11 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* @param {RemindTime[]} remindTimes
|
||||
*/
|
||||
const isSingleRemind = (remindTimes) => {
|
||||
return remindTimes.every(remindTime => remindTime.frequency === 'single')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isSingleRemind,
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
const { getNowDateStr } = require("./time");
|
||||
const moment = require('moment')
|
||||
|
||||
const chineseHoliday = {
|
||||
'2023-09-29': true,
|
||||
'2023-09-30': true,
|
||||
'2023-10-01': true,
|
||||
'2023-10-02': true,
|
||||
'2023-10-03': true,
|
||||
'2023-10-04': true,
|
||||
'2023-10-05': true,
|
||||
'2023-10-06': true,
|
||||
'2023-10-07': false,
|
||||
'2023-10-08': false,
|
||||
'2023-12-31': true,
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是工作日
|
||||
* @param {string} time 时间
|
||||
* @returns {boolean} 是否是工作日
|
||||
*/
|
||||
const judgeIsWorkDay = (time) => {
|
||||
const dateStr = getNowDateStr(time)
|
||||
// 是补班
|
||||
if (chineseHoliday[dateStr] === false) return true;
|
||||
// 是假期
|
||||
if (chineseHoliday[dateStr]) return false;
|
||||
// 是否是日常工作日
|
||||
const now = time ? new Date(time) : new Date();
|
||||
const nowDay = now.getDay();
|
||||
return nowDay !== 0 && nowDay !== 6
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定配置下一次的提醒时间以及对应中文显示
|
||||
* @param {RemindTime} remindTime 提醒时间配置
|
||||
*/
|
||||
const genNextRemindTimeWithCHS = (remindTime) => {
|
||||
const { frequency, time, daysOfWeek, daysOfMonth, dayOfYear } = remindTime;
|
||||
// 拆分时间
|
||||
const [hour, minute] = time.split(':');
|
||||
// 当前时间
|
||||
const nowMoment = moment();
|
||||
// 每天循环
|
||||
if (frequency === 'daily') {
|
||||
const remindMoment = moment().hour(hour).minute(minute);
|
||||
// 判断当前时间是否已经过了今天的提醒时间
|
||||
if (remindMoment.isSameOrBefore(nowMoment)) {
|
||||
// 如果已经过了,那么下次提醒时间为明天的提醒时间
|
||||
remindMoment.add(1, 'day');
|
||||
}
|
||||
return {
|
||||
nextRemindTime: remindMoment.format('YYYY-MM-DD HH:mm'),
|
||||
nextRemindTimeCHS: `每天 ${time}`,
|
||||
};
|
||||
}
|
||||
// 每周循环
|
||||
if (frequency === 'weekly') {
|
||||
// 也是取离当前时间点最近的某一天
|
||||
return daysOfWeek.reduce(({ nextRemindTime, nextRemindTimeCHS }, dayOfWeek) => {
|
||||
const remindMoment = moment().hour(hour).minute(minute).isoWeekday(dayOfWeek);
|
||||
// 判断当前时间是否已经过了本周的提醒时间
|
||||
if (remindMoment.isSameOrBefore(nowMoment)) {
|
||||
// 如果已经过了,那么下次提醒时间为下周的提醒时间
|
||||
remindMoment.add(1, 'week');
|
||||
}
|
||||
// 取最近的时间
|
||||
if (remindMoment.isBefore(moment(nextRemindTime))) {
|
||||
return {
|
||||
nextRemindTime: remindMoment.format('YYYY-MM-DD HH:mm'),
|
||||
nextRemindTimeCHS: `每${["周一", "周二", "周三", "周四", "周五", "周六", "周日"][dayOfWeek - 1]} ${time}`
|
||||
}
|
||||
}
|
||||
return {
|
||||
nextRemindTime,
|
||||
nextRemindTimeCHS,
|
||||
}
|
||||
}, { nextRemindTime: '2099-12-31 23:59', nextRemindTimeCHS: '' })
|
||||
}
|
||||
// 每月循环
|
||||
if (frequency === 'monthly') {
|
||||
// 也是取里当前时间点最近的某一天
|
||||
return daysOfMonth.reduce(({ nextRemindTime, nextRemindTimeCHS }, dayOfMonth) => {
|
||||
// 获取最近应该提醒的日期,如果一个月没有31天,那么就是最后一天
|
||||
const dayOfMonthNum = moment().daysInMonth();
|
||||
const remindMoment = moment().hour(hour).minute(minute).date(dayOfMonthNum < dayOfMonth ? dayOfMonthNum : dayOfMonth);
|
||||
// 判断当前时间是否已经过了本月的提醒时间
|
||||
if (remindMoment.isSameOrBefore(nowMoment)) {
|
||||
// 如果已经过了,那么下次提醒时间为下月的提醒时间,需要重新判断一下是否有31号
|
||||
const nextDayOfMonthNum = moment().add(1, 'month').daysInMonth();
|
||||
remindMoment.add(1, 'month').date(nextDayOfMonthNum < dayOfMonth ? nextDayOfMonthNum : dayOfMonth);
|
||||
}
|
||||
// 取最近的时间
|
||||
if (remindMoment.isBefore(moment(nextRemindTime))) {
|
||||
return {
|
||||
nextRemindTime: remindMoment.format('YYYY-MM-DD HH:mm'),
|
||||
nextRemindTimeCHS: `每月${dayOfMonth}日 ${time}`
|
||||
}
|
||||
}
|
||||
return {
|
||||
nextRemindTime,
|
||||
nextRemindTimeCHS,
|
||||
}
|
||||
}, { nextRemindTime: '2099-12-31 23:59', nextRemindTimeCHS: '' })
|
||||
}
|
||||
// 每年循环
|
||||
if (frequency === 'yearly') {
|
||||
// 月份,monont是从0开始
|
||||
const month = dayOfYear.split('-')[0] - 1;
|
||||
// 月份的日期
|
||||
const dayOfMonth = dayOfYear.split('-')[1];
|
||||
// 获取当月的时间,如果当月没有29号,那么就是最后一天
|
||||
const dayOfMonthNum = moment().month(month).daysInMonth();
|
||||
const remindMoment = moment().hour(hour).minute(minute).month(month).date(dayOfMonthNum < dayOfMonth ? dayOfMonthNum : dayOfMonth);
|
||||
// 判断当前时间是否已经过了今年的提醒时间
|
||||
if (remindMoment.isSameOrBefore(nowMoment)) {
|
||||
// 如果已经过了,那么下次提醒时间为明年的提醒时间,需要重新判断一下是否有29号
|
||||
const nextDayOfMonthNum = moment().add(1, 'year').month(month).daysInMonth();
|
||||
remindMoment.add(1, 'year').date(nextDayOfMonthNum < dayOfMonth ? nextDayOfMonthNum : dayOfMonth);
|
||||
}
|
||||
return {
|
||||
nextRemindTime: remindMoment.format('YYYY-MM-DD HH:mm'),
|
||||
nextRemindTimeCHS: `每年${dayOfYear} ${time}`
|
||||
};
|
||||
}
|
||||
// 工作日循环
|
||||
if (frequency === 'workday') {
|
||||
const remindMoment = moment().hour(hour).minute(minute);
|
||||
// 今天是否是工作日
|
||||
const isWorkday = judgeIsWorkDay();
|
||||
// 如果今天非工作日或者如果是工作日,且当前时间过了提醒时间,那么下次提醒时间为下次工作日的提醒时间
|
||||
if (!isWorkday || (isWorkday && remindMoment.isSameOrBefore(nowMoment))) {
|
||||
remindMoment.add(1, 'day');
|
||||
while (!judgeIsWorkDay(remindMoment)) {
|
||||
remindMoment.add(1, 'day');
|
||||
}
|
||||
}
|
||||
return {
|
||||
nextRemindTime: remindMoment.format('YYYY-MM-DD HH:mm'),
|
||||
nextRemindTimeCHS: `工作日 ${time}`
|
||||
};
|
||||
}
|
||||
// 非工作日循环
|
||||
if (frequency === 'holiday') {
|
||||
const remindMoment = moment().hour(hour).minute(minute);
|
||||
// 今天是否是工作日
|
||||
const isWorkday = judgeIsWorkDay();
|
||||
// 如果今天是工作日或者如果是非工作日,且当前时间过了提醒时间,那么下次提醒时间为下次非工作日的提醒时间
|
||||
if (isWorkday || (!isWorkday && remindMoment.isSameOrBefore(nowMoment))) {
|
||||
remindMoment.add(1, 'day');
|
||||
while (judgeIsWorkDay(remindMoment)) {
|
||||
remindMoment.add(1, 'day');
|
||||
}
|
||||
}
|
||||
return {
|
||||
nextRemindTime: remindMoment.format('YYYY-MM-DD HH:mm'),
|
||||
nextRemindTimeCHS: `节假日 ${time}`
|
||||
};
|
||||
}
|
||||
// 单次提醒
|
||||
// 此时的time是完整的时间,直接返回即可
|
||||
return {
|
||||
nextRemindTime: time,
|
||||
nextRemindTimeCHS: time,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下次提醒时间,在传入的所有时间配置中获取最早的一个
|
||||
* @param {RemindTime[]} remindTimes 提醒信息
|
||||
* @returns {{ nextRemindTime: string, nextRemindTimeCHS: string }} 下次提醒时间, 格式为yyyy-MM-dd HH:mm 以及对应中文显示
|
||||
*/
|
||||
module.exports.getNextRemindTime = (remindTimes) => {
|
||||
return remindTimes.reduce(({ nextRemindTime, nextRemindTimeCHS }, remindTime) => {
|
||||
const { nextRemindTime: nextTime, nextRemindTimeCHS: nextTimeCHS } = genNextRemindTimeWithCHS(remindTime)
|
||||
if (moment(nextTime).isBefore(moment(nextRemindTime))) {
|
||||
return {
|
||||
nextRemindTime: nextTime,
|
||||
nextRemindTimeCHS: nextTimeCHS,
|
||||
}
|
||||
}
|
||||
return {
|
||||
nextRemindTime,
|
||||
nextRemindTimeCHS,
|
||||
}
|
||||
}, { nextRemindTime: '2099-12-31 23:59', nextRemindTimeCHS: '' });
|
||||
}
|
204
utils/pb.js
204
utils/pb.js
@ -1,204 +0,0 @@
|
||||
const PocketBase = require('pocketbase/cjs')
|
||||
const { getNowStr } = require('./time')
|
||||
require('cross-fetch/polyfill')
|
||||
|
||||
const pb = new PocketBase('https://eggpb.imoaix.cn')
|
||||
|
||||
const manage404 = async (dbFunc) => {
|
||||
try {
|
||||
return await dbFunc()
|
||||
} catch (err) {
|
||||
console.log('errdfsfsfsfsdf', err)
|
||||
// 没有这个提醒就返回空
|
||||
if (err.message === "The requested resource wasn't found.") {
|
||||
return null
|
||||
} else throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新租户的token
|
||||
* @param {string} value 新的token
|
||||
*/
|
||||
const updateTenantAccessToken = async (value) => {
|
||||
await pb.collection('config').update('ugel8f0cpk0rut6', { value })
|
||||
console.log('reset access token success', value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户的token
|
||||
* @returns {string} 租户的token
|
||||
*/
|
||||
const getTenantAccessToken = async () => {
|
||||
const { value } = await pb.collection('config').getOne('ugel8f0cpk0rut6')
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新提醒
|
||||
* @param {Remind} remind 提醒内容
|
||||
*/
|
||||
const createRemind = async (remind) => {
|
||||
return await pb.collection('remind').create(remind)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新提醒
|
||||
* @param {string} remindId 提醒ID
|
||||
* @param {Remind} remind 提醒内容
|
||||
*/
|
||||
const updateRemind = async (remindId, remind) => {
|
||||
return await pb.collection('remind').update(remindId, remind)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提醒信息
|
||||
* @param {string} id 提醒ID
|
||||
* @returns {Remind | null}
|
||||
*/
|
||||
const getRemind = async (remindId) => manage404(async () =>
|
||||
await pb.collection('remind').getOne(remindId, { expand: 'owner' })
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取对应messageId对应的提醒
|
||||
* @param {string} messageId 消息ID
|
||||
* @returns {Remind | null}
|
||||
*/
|
||||
const getRemindByMessageId = async (messageId) => manage404(async () =>
|
||||
await pb.collection('remind').getFirstListItem(
|
||||
`messageId = "${messageId}" && enabled = true`
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* 根据messageId更新或者创建提醒
|
||||
* @param {Remind} remind 提醒内容
|
||||
*/
|
||||
const upsertRemindByMessageId = async (remind) => {
|
||||
const record = await getRemindByMessageId(remind.messageId)
|
||||
if (record) updateRemind(record.id, remind)
|
||||
else createRemind(remind)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前分钟应该提醒的所有提醒
|
||||
*/
|
||||
const getCurrTimeRemind = async () => manage404(async () => {
|
||||
const { items } = await pb.collection('remind').getList(1, 1000, {
|
||||
filter: `nextRemindTime = "${getNowStr()}" && enabled = true`,
|
||||
expand: 'owner'
|
||||
})
|
||||
return items
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取指定remind的pending状态的remindRecord
|
||||
* @param {string} remindId remind的id
|
||||
* @returns {RemindRecord | null} remindRecord
|
||||
*/
|
||||
const getPendingRemindRecord = async (remindId) => manage404(async () =>
|
||||
await pb.collection('remindRecord').getFirstListItem(
|
||||
`remindId = "${remindId}" && status = "pending"`
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取指定messageId状态的remindRecord
|
||||
* @param {string} remindId remind的id
|
||||
* @returns {RemindRecord | null} remindRecord
|
||||
*/
|
||||
const getRemindRecordByMessageId = async (messageId) => manage404(async () =>
|
||||
await pb.collection('remindRecord').getFirstListItem(
|
||||
`messageId = "${messageId}"`
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* 创建remindRecord
|
||||
* @param {string} remindId 提醒ID
|
||||
* @param {string} messageId 消息ID
|
||||
* @param {string} status 交互状态
|
||||
*/
|
||||
const createRemindRecord = async (remindId, messageId, status) => {
|
||||
const remindRecord = {
|
||||
remindId,
|
||||
messageId,
|
||||
status: status,
|
||||
remindTime: getNowStr(),
|
||||
interactTime: status === 'pending' ? '' : getNowStr(),
|
||||
}
|
||||
return await pb.collection('remindRecord').create(remindRecord)
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改指定remindRecord
|
||||
* @param {string} id remindRecord的id
|
||||
* @param {RemindRecord} record remindRecord的信息
|
||||
*/
|
||||
const updateRemindRecord = async (id, record) => {
|
||||
return await pb.collection('remindRecord').update(id, record)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定用户的信息
|
||||
* @param {string} userId 用户id
|
||||
* @returns {User} 用户信息
|
||||
*/
|
||||
const getUser = async (userId) => manage404(async () =>
|
||||
await pb.collection("user").getFirstListItem(`userId="${userId}"`)
|
||||
)
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
* @param {string} userId 用户id
|
||||
* @param {string} openId 用户openId
|
||||
* @returns {User} 用户信息
|
||||
*/
|
||||
const createUser = async (userId, openId) => {
|
||||
return await pb.collection("user").create({ userId, openId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
* @param {string} id 用户id
|
||||
* @param {User} data 用户信息
|
||||
* @returns {User} 用户信息
|
||||
*/
|
||||
const updateUser = async (id, data) => {
|
||||
return await pb.collection("user").update(id, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息,如果用户不存在则创建
|
||||
* @param {User} userInfo 用户信息
|
||||
* @returns {User} 用户信息
|
||||
*/
|
||||
const upsertUser = async (userInfo) => {
|
||||
const user = await getUser(userInfo.user_id);
|
||||
if (!user) return await createUser(userInfo.user_id, userInfo.open_id);
|
||||
// 如果用户信息没变化,直接返回
|
||||
if (user.openId === userInfo.open_id) {
|
||||
return user;
|
||||
}
|
||||
// 如果用户信息有变化,更新
|
||||
return await updateUser(user.id, {
|
||||
openId: userInfo.open_id,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
updateTenantAccessToken,
|
||||
getTenantAccessToken,
|
||||
createRemind,
|
||||
updateRemind,
|
||||
getRemind,
|
||||
getRemindByMessageId,
|
||||
upsertRemindByMessageId,
|
||||
getCurrTimeRemind,
|
||||
getPendingRemindRecord,
|
||||
getRemindRecordByMessageId,
|
||||
createRemindRecord,
|
||||
updateRemindRecord,
|
||||
upsertUser,
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
const fetch = require('node-fetch');
|
||||
const {
|
||||
getTenantAccessToken
|
||||
} = require('./pb');
|
||||
|
||||
/**
|
||||
* 发送卡片
|
||||
* @param {string} receive_id_type 消息接收者id类型 open_id/user_id/union_id/email/chat_id
|
||||
* @param {string} receive_id 消息接收者的ID,ID类型应与查询参数receive_id_type 对应
|
||||
* @param {string} msg_type 消息类型 包括:text、post、image、file、audio、media、sticker、interactive、share_chat、share_user
|
||||
* @param {string} content 消息内容,JSON结构序列化后的字符串。不同msg_type对应不同内容
|
||||
* @returns {string} 消息id
|
||||
*/
|
||||
module.exports.sendMsg = async (receive_id_type, receive_id, msg_type, content) => {
|
||||
const URL = `https://open.f.mioffice.cn/open-apis/im/v1/messages?receive_id_type=${receive_id_type}`
|
||||
const tenant_access_token = await getTenantAccessToken();
|
||||
const header = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${tenant_access_token}`
|
||||
}
|
||||
const body = { receive_id, msg_type, content }
|
||||
const res = await fetch(URL, {
|
||||
method: 'POST',
|
||||
headers: header,
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
const data = await res.json();
|
||||
console.log('sendMsg success', data);
|
||||
return data.data.message_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新卡片
|
||||
* @param {string} message_id 消息id
|
||||
* @param {*} content 消息内容,JSON结构序列化后的字符串。不同msg_type对应不同内容
|
||||
*/
|
||||
module.exports.updateCard = async (message_id, content) => {
|
||||
const URL = `https://open.f.mioffice.cn/open-apis/im/v1/messages/${message_id}`
|
||||
const tenant_access_token = await getTenantAccessToken();
|
||||
const header = {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Authorization': `Bearer ${tenant_access_token}`
|
||||
}
|
||||
const body = { content }
|
||||
const res = await fetch(URL, {
|
||||
method: 'PATCH',
|
||||
headers: header,
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
const data = await res.json();
|
||||
console.log('updateCard success', data);
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
const moment = require('moment')
|
||||
|
||||
/**
|
||||
* 获取当前时间的字符串
|
||||
* @returns {string} 当前时间的字符串 YYYY-MM-DD HH:mm
|
||||
* @example 2020-12-12 12:12
|
||||
*/
|
||||
module.exports.getNowStr = () => {
|
||||
return moment().format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日期的字符串
|
||||
* @param {string} time 时间
|
||||
* @returns {string} 当前日期的字符串 YYYY-MM-DD
|
||||
*/
|
||||
module.exports.getNowDateStr = (time) => {
|
||||
const targetTime = time ? new Date(time).getTime() : new Date().getTime()
|
||||
return moment(targetTime).format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定分钟后的时间字符串
|
||||
* @param {number} minutes 分钟数
|
||||
* @returns {string} 指定分钟后的时间字符串 YYYY-MM-DD HH:mm
|
||||
*/
|
||||
module.exports.getDelayedTimeStr = (minutes) => {
|
||||
return moment().add(minutes, 'minutes').format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
/**
|
||||
* 转化为当前时区的时间
|
||||
* @returns {string} 当前时间的字符串 YYYY-MM-DD HH:mm
|
||||
* @example 2020-12-12 12:12
|
||||
*/
|
||||
module.exports.trans2LocalTime = (time) => {
|
||||
return moment(new Date(time).getTime()).format('YYYY-MM-DD HH:mm')
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
module.exports.getYiYan = async () => {
|
||||
const URL = 'https://v1.hitokoto.cn/?c=i'
|
||||
const res = await fetch(URL)
|
||||
const { hitokoto } = await res.json()
|
||||
console.log('get YiYan success', hitokoto)
|
||||
return hitokoto
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "egg_fontend",
|
||||
"image": "micr.cloud.mioffice.cn/zhaoyingbo/dev:bun",
|
||||
"remoteUser": "bun",
|
||||
"containerUser": "bun",
|
||||
"forwardPorts": [5173],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"files.autoSave": "off",
|
||||
"editor.guides.bracketPairs": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
},
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"eamodio.gitlens",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"litiany4.umijs-plugin-model",
|
||||
"oderwat.indent-rainbow",
|
||||
"jock.svg",
|
||||
"aminer.codegeex",
|
||||
"ChakrounAnas.turbo-console-log",
|
||||
"Gruntfuggly.todo-tree",
|
||||
"MS-CEINTL.vscode-language-pack-zh-hans"
|
||||
]
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "bash -i /workspaces/egg_server/view/.devcontainer/initial.bash"
|
||||
}
|
@ -1 +0,0 @@
|
||||
echo "alias dev=\"cd /workspaces/egg_server/view && bun run dev\"" >> /home/node/.bashrc
|
@ -1,2 +0,0 @@
|
||||
*.js
|
||||
vite.config.ts
|
@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ['mantine'],
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
rules: {
|
||||
'import/extensions': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'no-console': 'off',
|
||||
},
|
||||
};
|
24
view/.gitignore
vendored
24
view/.gitignore
vendored
@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
@ -1 +0,0 @@
|
||||
module.exports = require('eslint-config-mantine/.prettierrc.js');
|
@ -1,12 +0,0 @@
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.story.@(js|jsx|ts|tsx)'],
|
||||
addons: ['@storybook/addon-essentials', '@storybook/addon-styling', 'storybook-dark-mode'],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
@ -1,25 +0,0 @@
|
||||
import '@mantine/core/styles.css';
|
||||
import React, { useEffect } from 'react';
|
||||
import { addons } from '@storybook/preview-api';
|
||||
import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
|
||||
import { MantineProvider, useMantineColorScheme } from '@mantine/core';
|
||||
import { theme } from '../src/theme';
|
||||
|
||||
const channel = addons.getChannel();
|
||||
|
||||
function ColorSchemeWrapper({ children }: { children: React.ReactNode }) {
|
||||
const { setColorScheme } = useMantineColorScheme();
|
||||
const handleColorScheme = (value: boolean) => setColorScheme(value ? 'dark' : 'light');
|
||||
|
||||
useEffect(() => {
|
||||
channel.on(DARK_MODE_EVENT_NAME, handleColorScheme);
|
||||
return () => channel.off(DARK_MODE_EVENT_NAME, handleColorScheme);
|
||||
}, [channel]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export const decorators = [
|
||||
(renderStory: any) => <ColorSchemeWrapper>{renderStory()}</ColorSchemeWrapper>,
|
||||
(renderStory: any) => <MantineProvider theme={theme}>{renderStory()}</MantineProvider>,
|
||||
];
|
@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width, user-scalable=no"
|
||||
/>
|
||||
<title>Vite + Mantine App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,12 +0,0 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
moduleNameMapper: {
|
||||
'^@test-utils': '<rootDir>/test-utils',
|
||||
'\\.css$': 'identity-obj-proxy',
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.ts?$': 'ts-jest',
|
||||
},
|
||||
};
|
@ -1,26 +0,0 @@
|
||||
require('@testing-library/jest-dom/extend-expect');
|
||||
|
||||
const { getComputedStyle } = window;
|
||||
window.getComputedStyle = (elt) => getComputedStyle(elt);
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
class ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
window.ResizeObserver = ResizeObserver;
|
@ -1,85 +0,0 @@
|
||||
{
|
||||
"name": "mantine-vite-template",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src",
|
||||
"prettier": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
|
||||
"jest": "jest",
|
||||
"jest:watch": "jest --watch",
|
||||
"test": "npm run typecheck && npm run prettier && npm run lint && npm run jest && npm run build",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/carousel": "^7.0.0",
|
||||
"@mantine/code-highlight": "^7.0.0",
|
||||
"@mantine/core": "^7.0.0",
|
||||
"@mantine/dates": "^7.0.0",
|
||||
"@mantine/dropzone": "^7.0.0",
|
||||
"@mantine/form": "^7.0.0",
|
||||
"@mantine/hooks": "^7.0.0",
|
||||
"@mantine/modals": "^7.0.0",
|
||||
"@mantine/notifications": "^7.0.0",
|
||||
"@mantine/nprogress": "^7.0.0",
|
||||
"@mantine/spotlight": "^7.0.0",
|
||||
"@mantine/tiptap": "^7.0.0",
|
||||
"@tabler/icons-react": "^2.34.0",
|
||||
"@tiptap/extension-link": "^2.1.11",
|
||||
"@tiptap/react": "^2.1.11",
|
||||
"@tiptap/starter-kit": "^2.1.11",
|
||||
"dayjs": "^1.11.10",
|
||||
"embla-carousel-react": "^8.0.0-rc14",
|
||||
"moment": "^2.29.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.11.2",
|
||||
"pocketbase": "^0.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^7.0.18",
|
||||
"@storybook/addon-interactions": "^7.0.18",
|
||||
"@storybook/addon-links": "^7.0.18",
|
||||
"@storybook/addon-styling": "^1.0.8",
|
||||
"@storybook/blocks": "^7.0.18",
|
||||
"@storybook/react": "^7.0.18",
|
||||
"@storybook/react-vite": "^7.0.18",
|
||||
"@storybook/testing-library": "^0.0.14-next.2",
|
||||
"@testing-library/dom": "^9.3.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/jest": "^29.5.1",
|
||||
"@types/react": "^18.2.7",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.7",
|
||||
"@typescript-eslint/parser": "^5.59.7",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-mantine": "2.0.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-preset-mantine": "1.6.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "^2.8.8",
|
||||
"prop-types": "^15.8.1",
|
||||
"storybook": "^7.0.18",
|
||||
"storybook-dark-mode": "^3.0.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.9"
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '36em',
|
||||
'mantine-breakpoint-sm': '48em',
|
||||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -1,16 +0,0 @@
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
import { Router } from './Router';
|
||||
import { theme } from './theme';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<MantineProvider theme={theme}>
|
||||
<Notifications />
|
||||
<Router />
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
import { HomePage } from './pages/Home.page';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <HomePage />,
|
||||
},
|
||||
]);
|
||||
|
||||
export function Router() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { Button, Group, useMantineColorScheme } from '@mantine/core';
|
||||
|
||||
export function ColorSchemeToggle() {
|
||||
const { setColorScheme } = useMantineColorScheme();
|
||||
|
||||
return (
|
||||
<Group justify="center" mt="xl">
|
||||
<Button onClick={() => setColorScheme('light')}>Light</Button>
|
||||
<Button onClick={() => setColorScheme('dark')}>Dark</Button>
|
||||
<Button onClick={() => setColorScheme('auto')}>Auto</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
@ -1,302 +0,0 @@
|
||||
import { Accordion, Button, Select, MultiSelect, Center, Text } from '@mantine/core';
|
||||
import { DatePicker, DateTimePicker, TimeInput } from '@mantine/dates';
|
||||
import { useUncontrolled } from '@mantine/hooks';
|
||||
import { useState } from 'react';
|
||||
import { IconCalendar, IconClock } from '@tabler/icons-react';
|
||||
import moment from 'moment';
|
||||
import accordionClasses from './style.module.css';
|
||||
import { get202401Date, getDateFrom202401Date, getMMDDFromDate, getFullTimeStr, getTimeStr, getDateFromMMDD } from '@/utils/time';
|
||||
/**
|
||||
* 提醒时间
|
||||
* 为了支持多个时间点提醒,将时间存成数组
|
||||
*/
|
||||
interface RemindTime {
|
||||
/**
|
||||
* 重复类型
|
||||
* single: 一次性
|
||||
* daily: 每天
|
||||
* weekly: 每周
|
||||
* monthly: 每月
|
||||
* yearly: 每年
|
||||
* workday: 工作日
|
||||
* holiday: 节假日
|
||||
*/
|
||||
frequency:
|
||||
| 'single'
|
||||
| 'daily'
|
||||
| 'weekly'
|
||||
| 'monthly'
|
||||
| 'yearly'
|
||||
| 'workday'
|
||||
| 'holiday';
|
||||
/**
|
||||
* 提醒时间,格式为HH:mm, single类型时仅作展示用,类型为yyyy-MM-dd HH:mm
|
||||
*/
|
||||
time: string;
|
||||
/**
|
||||
* 星期几[1-7],当frequency为weekly时有效
|
||||
*/
|
||||
daysOfWeek?: number[];
|
||||
/**
|
||||
* 每月的几号[1-31],当frequency为monthly时有效
|
||||
*/
|
||||
daysOfMonth?: number[];
|
||||
/**
|
||||
* 每年的哪天提醒,当frequency为 yearly 时有效,格式为MM-dd
|
||||
*/
|
||||
dayOfYear?: string;
|
||||
|
||||
/**
|
||||
* fuck ts
|
||||
*/
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表情包列表
|
||||
*/
|
||||
const emojiList = ['🍅', '🍆', '🍌', '🍎', '🥝', '🥔', '🥕', '🌽', '🌶', '🍓', '🥬', '🍑', '🥭', '🥥', '🫐', '🍇', '🍉'];
|
||||
|
||||
/**
|
||||
* 时间类型列表
|
||||
*/
|
||||
const frequencyType = [
|
||||
{ label: '一次性', value: 'single' },
|
||||
{ label: '每天', value: 'daily' },
|
||||
{ label: '每周', value: 'weekly' },
|
||||
{ label: '每月', value: 'monthly' },
|
||||
{ label: '每年', value: 'yearly' },
|
||||
{ label: '工作日', value: 'workday' },
|
||||
{ label: '节假日', value: 'holiday' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 星期值列表
|
||||
*/
|
||||
const weekdayType = [
|
||||
{ label: '星期一', value: '1' },
|
||||
{ label: '星期二', value: '2' },
|
||||
{ label: '星期三', value: '3' },
|
||||
{ label: '星期四', value: '4' },
|
||||
{ label: '星期五', value: '5' },
|
||||
{ label: '星期六', value: '6' },
|
||||
{ label: '星期日', value: '7' },
|
||||
];
|
||||
/**
|
||||
* 获取中文时间
|
||||
*/
|
||||
const getCHSTime = (remindTime: RemindTime) => {
|
||||
const errText = <Text span c="red" size="md">未选择</Text>;
|
||||
const getWeekdayCHS = (day: number) => ['', '一', '二', '三', '四', '五', '六', '日'][day];
|
||||
if (remindTime.frequency === 'single') return remindTime.time;
|
||||
if (remindTime.frequency === 'daily') return `每天的${remindTime.time}`;
|
||||
if (remindTime.frequency === 'workday') return `工作日的${remindTime.time}`;
|
||||
if (remindTime.frequency === 'holiday') return `节假日的${remindTime.time}`;
|
||||
if (remindTime.frequency === 'monthly') {
|
||||
return (
|
||||
<span>
|
||||
每月
|
||||
{remindTime.daysOfMonth?.join(',') || errText}
|
||||
日的
|
||||
{remindTime.time}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (remindTime.frequency === 'weekly') {
|
||||
return (
|
||||
<span>
|
||||
每周
|
||||
{remindTime.daysOfWeek?.map(getWeekdayCHS)?.join(',') || errText}
|
||||
的
|
||||
{remindTime.time}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (remindTime.frequency === 'yearly') {
|
||||
return (
|
||||
<span>
|
||||
每年
|
||||
{remindTime.dayOfYear || errText}
|
||||
的
|
||||
{remindTime.time}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return '每周一、日的17:00';
|
||||
};
|
||||
|
||||
const MultiTimePicker = ({ remindTimes, onChange }: {
|
||||
remindTimes: RemindTime[];
|
||||
onChange: (val: RemindTime[]) => void
|
||||
}) => {
|
||||
const [openIndex, setOpenIndex] = useState<string | null>('');
|
||||
const [times, handleChange] = useUncontrolled({
|
||||
value: remindTimes,
|
||||
defaultValue: [],
|
||||
finalValue: [],
|
||||
onChange,
|
||||
});
|
||||
|
||||
const createTime = () => {
|
||||
handleChange([{ frequency: 'single', time: getFullTimeStr() }, ...times]);
|
||||
setOpenIndex('0');
|
||||
};
|
||||
|
||||
const changeTime = (idx: number, type: keyof RemindTime, val: RemindTime[keyof RemindTime]) => {
|
||||
const curItem = { ...times[idx] };
|
||||
let finalVal = val;
|
||||
// 数据处理
|
||||
if (type === 'frequency') {
|
||||
// 必须选一个,默认single
|
||||
if (!finalVal) finalVal = 'single';
|
||||
if (finalVal === 'single') {
|
||||
// 单次提醒的时间需要设置为 yyyy-MM-dd HH:mm
|
||||
curItem.time = getFullTimeStr();
|
||||
} else {
|
||||
// 其余选项的时间都是 HH:mm
|
||||
curItem.time = getTimeStr();
|
||||
}
|
||||
// 清理其他内容
|
||||
curItem.dayOfYear = '';
|
||||
curItem.daysOfWeek = [];
|
||||
curItem.daysOfMonth = [];
|
||||
}
|
||||
if (type === 'time') {
|
||||
if (curItem.frequency === 'single') {
|
||||
// 这里的时间是完整的时间,需要用moment转一下
|
||||
finalVal = getFullTimeStr(finalVal);
|
||||
}
|
||||
}
|
||||
if (type === 'daysOfWeek') {
|
||||
finalVal = finalVal.map(Number).sort((a: number, b: number) => a - b);
|
||||
}
|
||||
if (type === 'daysOfMonth') {
|
||||
finalVal = finalVal.map(getDateFrom202401Date).sort((a: number, b: number) => a - b);
|
||||
}
|
||||
if (type === 'dayOfYear') {
|
||||
finalVal = getMMDDFromDate(finalVal);
|
||||
}
|
||||
console.log('finalVal', finalVal);
|
||||
// 赋值
|
||||
curItem[type] = finalVal;
|
||||
handleChange([
|
||||
...times.slice(0, idx),
|
||||
curItem,
|
||||
...times.slice(idx + 1),
|
||||
]);
|
||||
};
|
||||
|
||||
const delTime = (idx: number) => handleChange([
|
||||
...times.slice(0, idx),
|
||||
...times.slice(idx + 1),
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button fullWidth variant="default" my="md" onClick={() => createTime()}>新增提醒时间</Button>
|
||||
<Accordion classNames={accordionClasses} value={openIndex} onChange={setOpenIndex}>
|
||||
{times.map((item, idx) => (
|
||||
<Accordion.Item key={idx} value={String(idx)}>
|
||||
<Accordion.Control
|
||||
icon={emojiList[(times.length - idx - 1) % emojiList.length]}
|
||||
>
|
||||
{getCHSTime(item)}
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Select
|
||||
mb="xs"
|
||||
label="重复类型"
|
||||
description={['workday', 'holiday'].includes(item.frequency) && '计算含法定假日,类似周一到周五固定值请选择每周循环'}
|
||||
withAsterisk
|
||||
data={frequencyType}
|
||||
checkIconPosition="right"
|
||||
allowDeselect={false}
|
||||
value={item.frequency}
|
||||
onChange={(val: RemindTime['frequency']) => changeTime(idx, 'frequency', val)}
|
||||
/>
|
||||
{/* 单次提醒 */}
|
||||
{item.frequency === 'single' && (
|
||||
<DateTimePicker
|
||||
label="提醒时间"
|
||||
leftSection={<IconCalendar style={{ width: 18, height: 18 }} stroke={1.5} />}
|
||||
withAsterisk
|
||||
minDate={new Date()}
|
||||
valueFormat="YYYY-MM-DD HH:mm"
|
||||
value={new Date(item.time)}
|
||||
dropdownType="modal"
|
||||
onChange={(val: any) => changeTime(idx, 'time', val)}
|
||||
/>
|
||||
)}
|
||||
{/* 除了单次提醒都需要设置HH:mm的时间 */}
|
||||
{item.frequency !== 'single' && (
|
||||
<TimeInput
|
||||
my="xs"
|
||||
label="提醒时间"
|
||||
withAsterisk
|
||||
value={item.time}
|
||||
leftSection={<IconClock style={{ width: 18, height: 18 }} stroke={1.5} />}
|
||||
onChange={event => changeTime(idx, 'time', event.currentTarget.value)}
|
||||
/>
|
||||
)}
|
||||
{/* 每周循环,选择星期几提醒 */}
|
||||
{item.frequency === 'weekly' && (
|
||||
<MultiSelect
|
||||
my="xs"
|
||||
label="星期"
|
||||
withAsterisk
|
||||
data={weekdayType}
|
||||
checkIconPosition="right"
|
||||
error={item.daysOfWeek?.length === 0 ? '请至少选择一个星期' : ''}
|
||||
value={item.daysOfWeek?.map(String)}
|
||||
onChange={(val: string[]) => changeTime(idx, 'daysOfWeek', val)}
|
||||
/>
|
||||
)}
|
||||
{/* 每月循环,选择1-31日期 */}
|
||||
{item.frequency === 'monthly' && (
|
||||
<>
|
||||
<Text size="sm" fw={500}>日期</Text>
|
||||
<Text size="xs" c="dimmed">如果当月无对应日期,将在该月最后一天提醒</Text>
|
||||
<Center>
|
||||
<DatePicker
|
||||
my="xs"
|
||||
defaultDate={new Date(2024, 0)}
|
||||
type="multiple"
|
||||
size="md"
|
||||
hideWeekdays
|
||||
hideOutsideDates
|
||||
value={item.daysOfMonth?.map(get202401Date)}
|
||||
onChange={val => changeTime(idx, 'daysOfMonth', val)}
|
||||
styles={{
|
||||
calendarHeader: { display: 'none' },
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
</>
|
||||
)}
|
||||
{/* 每年循环,选择日期 */}
|
||||
{item.frequency === 'yearly' && (
|
||||
<>
|
||||
<Text size="sm" fw={500}>日期</Text>
|
||||
<Text size="xs" c="dimmed">如果当月无对应日期,将在该月最后一天提醒</Text>
|
||||
<Center>
|
||||
<DatePicker
|
||||
my="xs"
|
||||
size="md"
|
||||
value={getDateFromMMDD(item.dayOfYear || '')}
|
||||
onChange={val => changeTime(idx, 'dayOfYear', val)}
|
||||
minDate={moment().startOf('year').toDate()}
|
||||
maxDate={moment().endOf('year').toDate()}
|
||||
/>
|
||||
</Center>
|
||||
</>
|
||||
)}
|
||||
<Button fullWidth variant="light" color="red" my="md" onClick={() => delTime(idx)} id={`timePickerAnchor_${idx}`}>删除</Button>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiTimePicker;
|
@ -1,27 +0,0 @@
|
||||
.root {
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
}
|
||||
|
||||
.item {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
border: rem(1px) solid transparent;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
transition: transform 150ms ease;
|
||||
|
||||
&[data-active] {
|
||||
transform: scale(1.03);
|
||||
z-index: 1;
|
||||
background-color: var(--mantine-color-body);
|
||||
border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
|
||||
box-shadow: var(--mantine-shadow-md);
|
||||
border-radius: var(--mantine-radius-md);
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
&[data-rotate] {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
.title {
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
font-size: rem(100px);
|
||||
font-weight: 900;
|
||||
letter-spacing: rem(-2px);
|
||||
|
||||
@media (max-width: $mantine-breakpoint-md) {
|
||||
font-size: rem(50px);
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { Welcome } from './Welcome';
|
||||
|
||||
export default {
|
||||
title: 'Welcome',
|
||||
};
|
||||
|
||||
export const Usage = () => <Welcome />;
|
@ -1,12 +0,0 @@
|
||||
import { render, screen } from '@test-utils';
|
||||
import { Welcome } from './Welcome';
|
||||
|
||||
describe('Welcome component', () => {
|
||||
it('has correct Vite guide link', () => {
|
||||
render(<Welcome />);
|
||||
expect(screen.getByText('this guide')).toHaveAttribute(
|
||||
'href',
|
||||
'https://mantine.dev/guides/vite/'
|
||||
);
|
||||
});
|
||||
});
|
@ -1,23 +0,0 @@
|
||||
import { Title, Text, Anchor } from '@mantine/core';
|
||||
import classes from './Welcome.module.css';
|
||||
|
||||
export function Welcome() {
|
||||
return (
|
||||
<>
|
||||
<Title className={classes.title} ta="center" mt={100}>
|
||||
Welcome to{' '}
|
||||
<Text inherit variant="gradient" component="span" gradient={{ from: 'pink', to: 'yellow' }}>
|
||||
Mantine
|
||||
</Text>
|
||||
</Title>
|
||||
<Text c="dimmed" ta="center" size="lg" maw={580} mx="auto" mt="xl">
|
||||
This starter Vite project includes a minimal setup, if you want to learn more on Mantine +
|
||||
Vite integration follow{' '}
|
||||
<Anchor href="https://mantine.dev/guides/vite/" size="lg">
|
||||
this guide
|
||||
</Anchor>
|
||||
. To get started edit pages/Home.page.tsx file.
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 163 163"><path fill="#339AF0" d="M162.162 81.5c0-45.011-36.301-81.5-81.08-81.5C36.301 0 0 36.489 0 81.5 0 126.51 36.301 163 81.081 163s81.081-36.49 81.081-81.5z"/><path fill="#fff" d="M65.983 43.049a6.234 6.234 0 00-.336 6.884 6.14 6.14 0 001.618 1.786c9.444 7.036 14.866 17.794 14.866 29.52 0 11.726-5.422 22.484-14.866 29.52a6.145 6.145 0 00-1.616 1.786 6.21 6.21 0 00-.694 4.693 6.21 6.21 0 001.028 2.186 6.151 6.151 0 006.457 2.319 6.154 6.154 0 002.177-1.035 50.083 50.083 0 007.947-7.39h17.493c3.406 0 6.174-2.772 6.174-6.194s-2.762-6.194-6.174-6.194h-9.655a49.165 49.165 0 004.071-19.69 49.167 49.167 0 00-4.07-19.692h9.66c3.406 0 6.173-2.771 6.173-6.194 0-3.422-2.762-6.193-6.173-6.193H82.574a50.112 50.112 0 00-7.952-7.397 6.15 6.15 0 00-4.578-1.153 6.189 6.189 0 00-4.055 2.438h-.006z"/><path fill="#fff" fill-rule="evenodd" d="M56.236 79.391a9.342 9.342 0 01.632-3.608 9.262 9.262 0 011.967-3.077 9.143 9.143 0 012.994-2.063 9.06 9.06 0 017.103 0 9.145 9.145 0 012.995 2.063 9.262 9.262 0 011.967 3.077 9.339 9.339 0 01-2.125 10.003 9.094 9.094 0 01-6.388 2.63 9.094 9.094 0 01-6.39-2.63 9.3 9.3 0 01-2.755-6.395z" clip-rule="evenodd"/></svg>
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,4 +0,0 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
|
@ -1,271 +0,0 @@
|
||||
import { Box, Button, Center, Collapse, NumberInput, SegmentedControl, Select, Switch, TextInput } from '@mantine/core';
|
||||
import { isNotEmpty, useForm } from '@mantine/form';
|
||||
import { useSetState } from '@mantine/hooks';
|
||||
import { IconAdjustmentsCode, IconCards } from '@tabler/icons-react';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { useEffect } from 'react';
|
||||
import MultiTimePicker from '@/components/MultiTimePicker';
|
||||
import { getImgList } from '@/server';
|
||||
|
||||
export function HomePage() {
|
||||
// 从参数中获取订阅者信息
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const [state, setState] = useSetState({
|
||||
type: 'card',
|
||||
imageKeyList: [{ label: 'loading...', value: 'loading...' }],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getImgList().then((records) => {
|
||||
setState({
|
||||
imageKeyList: records.map(({ key, description }) => ({ label: description, value: key })),
|
||||
});
|
||||
}).catch(err => console.error(err));
|
||||
}, []);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
// card
|
||||
title: '',
|
||||
imageKey: '',
|
||||
content: '',
|
||||
confirmText: '',
|
||||
cancelText: '',
|
||||
delayText: '',
|
||||
// template
|
||||
pendingTemplateId: '',
|
||||
interactedTemplateId: '',
|
||||
confirmedTemplateId: '',
|
||||
cancelededTemplateId: '',
|
||||
delayedTemplateId: '',
|
||||
// time
|
||||
remindTimes: [],
|
||||
// subscriber
|
||||
subscriberType: searchParams.get('subscriberType') ?? '',
|
||||
subscriberId: searchParams.get('subscriberId') ?? '',
|
||||
// others
|
||||
needReply: false,
|
||||
delayTime: 15,
|
||||
},
|
||||
validate: {
|
||||
content: state.type === 'card' ? isNotEmpty('请填入卡片内容') : () => null,
|
||||
subscriberType: isNotEmpty('请选择订阅者类型'),
|
||||
subscriberId: isNotEmpty('请输入订阅者ID'),
|
||||
remindTimes: (value) => {
|
||||
// 这个地方只能使用notify提醒
|
||||
if (value.length === 0) {
|
||||
notifications.show({
|
||||
message: '请至少选择一个用来提醒的时间! 🤥',
|
||||
color: 'red',
|
||||
autoClose: 2000,
|
||||
});
|
||||
return '请至少选择一个用来提醒的时间! 🤥';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
pendingTemplateId: state.type === 'template' ? isNotEmpty('请输入模板ID') : () => null,
|
||||
interactedTemplateId: (
|
||||
value,
|
||||
{ needReply, confirmedTemplateId, cancelededTemplateId, delayedTemplateId }
|
||||
) => {
|
||||
if (
|
||||
needReply
|
||||
&& !value && !confirmedTemplateId && !cancelededTemplateId && !delayedTemplateId
|
||||
) {
|
||||
return '请输入模板ID';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
confirmedTemplateId: (
|
||||
value,
|
||||
{ needReply, interactedTemplateId }
|
||||
) => {
|
||||
if (needReply && !value && !interactedTemplateId) {
|
||||
return '请输入模板ID';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
cancelededTemplateId: (
|
||||
value,
|
||||
{ needReply, interactedTemplateId }
|
||||
) => {
|
||||
if (needReply && !value && !interactedTemplateId) {
|
||||
return '请输入模板ID';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
delayedTemplateId: (
|
||||
value,
|
||||
{ needReply, interactedTemplateId }
|
||||
) => {
|
||||
if (needReply && !value && !interactedTemplateId) {
|
||||
return '请输入模板ID';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Box maw={340} mx="auto" py="lg">
|
||||
<form onSubmit={form.onSubmit((values) => console.log(values))}>
|
||||
<Center>
|
||||
<SegmentedControl
|
||||
mb="xs"
|
||||
value={state.type}
|
||||
onChange={(value) => setState({ type: value })}
|
||||
data={[
|
||||
{
|
||||
value: 'card',
|
||||
label: (
|
||||
<Center>
|
||||
<IconAdjustmentsCode size={16} />
|
||||
<Box ml={10}>卡片</Box>
|
||||
</Center>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'template',
|
||||
label: (
|
||||
<Center>
|
||||
<IconCards size={16} />
|
||||
<Box ml={10}>模板</Box>
|
||||
</Center>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Center>
|
||||
{state.type === 'card' && (
|
||||
<>
|
||||
<TextInput
|
||||
mb="xs"
|
||||
label="卡片标题"
|
||||
description="不填默认:🍳小煎蛋提醒!"
|
||||
{...form.getInputProps('title')}
|
||||
/>
|
||||
<TextInput
|
||||
mb="xs"
|
||||
withAsterisk
|
||||
label="卡片内容"
|
||||
{...form.getInputProps('content')}
|
||||
/>
|
||||
<Select
|
||||
mb="xs"
|
||||
label="图片Key"
|
||||
data={state.imageKeyList}
|
||||
description="如果需要添加图片,找zhaoyingbo"
|
||||
checkIconPosition="right"
|
||||
searchable
|
||||
{...form.getInputProps('imageKey')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{state.type === 'template' && (
|
||||
<>
|
||||
<TextInput
|
||||
mb="xs"
|
||||
withAsterisk
|
||||
label="提醒卡片模板ID"
|
||||
{...form.getInputProps('pendingTemplateId')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Switch
|
||||
my="md"
|
||||
label="重复提醒 & 结果确认"
|
||||
description={`如提醒后未回复,将在 ${form.values.delayTime}min 之后重新提醒`}
|
||||
{...form.getInputProps('needReply')}
|
||||
/>
|
||||
<Collapse in={form.values.needReply}>
|
||||
<NumberInput
|
||||
mb="md"
|
||||
withAsterisk
|
||||
label="提醒时间间隔"
|
||||
description="单位:分钟"
|
||||
min={1}
|
||||
max={1440}
|
||||
{...form.getInputProps('delayTime')}
|
||||
/>
|
||||
{state.type === 'card' && (
|
||||
<>
|
||||
<TextInput
|
||||
mb="xs"
|
||||
label="确认操作文字"
|
||||
description="不填默认:完成"
|
||||
{...form.getInputProps('confirmText')}
|
||||
/>
|
||||
<TextInput
|
||||
mb="xs"
|
||||
label="取消操作文字"
|
||||
description="不填不显示交互按钮"
|
||||
{...form.getInputProps('cancelText')}
|
||||
/>
|
||||
<TextInput
|
||||
mb="xs"
|
||||
label="延迟操作文字"
|
||||
description="不填不显示交互按钮"
|
||||
{...form.getInputProps('delayText')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{state.type === 'template' && (
|
||||
<>
|
||||
<TextInput
|
||||
mb="xs"
|
||||
label="交互后结果模板ID"
|
||||
description="如果填了这个则不需要下边三个"
|
||||
{...form.getInputProps('interactedTemplateId')}
|
||||
/>
|
||||
<TextInput
|
||||
mb="xs"
|
||||
label="确认结果模板ID"
|
||||
{...form.getInputProps('confirmedTemplateId')}
|
||||
/>
|
||||
<TextInput
|
||||
mb="xs"
|
||||
label="取消结果模板ID"
|
||||
{...form.getInputProps('cancelededTemplateId')}
|
||||
/>
|
||||
<TextInput
|
||||
mb="xs"
|
||||
label="延迟结果模板ID"
|
||||
{...form.getInputProps('delayedTemplateId')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Collapse>
|
||||
<MultiTimePicker
|
||||
remindTimes={form.values.remindTimes}
|
||||
onChange={(val: any) => form.setValues({ remindTimes: val })}
|
||||
/>
|
||||
<Select
|
||||
my="xs"
|
||||
withAsterisk
|
||||
label="订阅者类型"
|
||||
data={['open_id', 'user_id', 'union_id', 'email', 'chat_id']}
|
||||
description="可以和Bot说info获取"
|
||||
checkIconPosition="right"
|
||||
disabled={searchParams.has('subscriberType')}
|
||||
{...form.getInputProps('subscriberType')}
|
||||
/>
|
||||
<TextInput
|
||||
my="xs"
|
||||
withAsterisk
|
||||
label="订阅者ID"
|
||||
disabled={searchParams.has('subscriberId')}
|
||||
{...form.getInputProps('subscriberId')}
|
||||
/>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="light"
|
||||
my="lg"
|
||||
type="submit"
|
||||
>
|
||||
确认提交
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('https://eggpb.imoaix.cn');
|
||||
|
||||
const manage404 = async (dbFunc: any) => {
|
||||
try {
|
||||
const res = await dbFunc();
|
||||
return res;
|
||||
} catch (err: any) {
|
||||
// 没有这个提醒就返回空
|
||||
if (err.message === "The requested resource wasn't found.") {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const getImgList = async () => {
|
||||
const records = await pb.collection('remindImgs').getFullList({
|
||||
sort: '-created',
|
||||
});
|
||||
return records;
|
||||
};
|
||||
|
||||
export { getImgList, manage404 };
|
@ -1,5 +0,0 @@
|
||||
import { createTheme } from '@mantine/core';
|
||||
|
||||
export const theme = createTheme({
|
||||
/** Put your mantine theme override here */
|
||||
});
|
@ -1,42 +0,0 @@
|
||||
import moment from 'moment';
|
||||
|
||||
/**
|
||||
* 获取当前完整时间
|
||||
* @returns 时间,格式为 YYYY-MM-DD HH:mm
|
||||
*/
|
||||
const getFullTimeStr = (time?: string) => moment(time).format('YYYY-MM-DD HH:mm');
|
||||
|
||||
/**
|
||||
* 获取当前时分
|
||||
* @returns 时间,格式为 HH:mm
|
||||
*/
|
||||
const getTimeStr = (time?: string) => moment(time).format('HH:mm');
|
||||
|
||||
/**
|
||||
* 获取202401指定日期的Date对象
|
||||
*/
|
||||
const get202401Date = (dayofMonth: number) => moment('202401', 'YYYYMM').date(dayofMonth).toDate();
|
||||
|
||||
/**
|
||||
* 从202401日期的Date对象提取出是几号
|
||||
*/
|
||||
const getDateFrom202401Date = (time: string) => Number(moment(time).format('DD'));
|
||||
|
||||
/**
|
||||
* 从Date对象中获取MM-DD格式的时间
|
||||
*/
|
||||
const getMMDDFromDate = (time: string) => moment(time).format('MM-DD');
|
||||
|
||||
/**
|
||||
* 根据MM-DD格式的时间获取Date对象
|
||||
*/
|
||||
const getDateFromMMDD = (time: string) => moment(time, 'MM-DD').toDate();
|
||||
|
||||
export {
|
||||
getFullTimeStr,
|
||||
getTimeStr,
|
||||
get202401Date,
|
||||
getDateFrom202401Date,
|
||||
getMMDDFromDate,
|
||||
getDateFromMMDD,
|
||||
};
|
1
view/src/vite-env.d.ts
vendored
1
view/src/vite-env.d.ts
vendored
@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
@ -1,5 +0,0 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
export * from '@testing-library/react';
|
||||
export { render } from './render';
|
||||
export { userEvent };
|
@ -1,11 +0,0 @@
|
||||
import { render as testingLibraryRender } from '@testing-library/react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import { theme } from '../src/theme';
|
||||
|
||||
export function render(ui: React.ReactNode) {
|
||||
return testingLibraryRender(<>{ui}</>, {
|
||||
wrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<MantineProvider theme={theme}>{children}</MantineProvider>
|
||||
),
|
||||
});
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@test-utils": ["./test-utils"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "test-utils"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
// 关键代码
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
10146
view/yarn.lock
10146
view/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user