feat: 改用bun进行完整实现

This commit is contained in:
zhaoyingbo 2024-03-04 03:06:44 +00:00
parent be05643c03
commit c1a4890eec
64 changed files with 58 additions and 15634 deletions

View File

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

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

BIN
bun.lockb

Binary file not shown.

13
index.ts Normal file
View 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
});

View File

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

View File

@ -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/).

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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!")
}

View File

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

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

View File

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

View File

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

View File

@ -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
}
// 需要回复的卡片下次提醒时间是delayTimemin之后的时间
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)
}
}

View File

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

@ -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} textresult对应的
* ${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 textpostimagefileaudiomediastickerinteractiveshare_chatshare_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;
};
}

View File

@ -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",
},
},
});
};

View File

@ -1,11 +0,0 @@
/**
*
* @param {RemindTime[]} remindTimes
*/
const isSingleRemind = (remindTimes) => {
return remindTimes.every(remindTime => remindTime.frequency === 'single')
}
module.exports = {
isSingleRemind,
}

View File

@ -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: '' });
}

View File

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

View File

@ -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 消息接收者的IDID类型应与查询参数receive_id_type 对应
* @param {string} msg_type 消息类型 包括textpostimagefileaudiomediastickerinteractiveshare_chatshare_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);
}

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
echo "alias dev=\"cd /workspaces/egg_server/view && bun run dev\"" >> /home/node/.bashrc

View File

@ -1,2 +0,0 @@
*.js
vite.config.ts

View File

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

@ -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?

View File

@ -1 +0,0 @@
module.exports = require('eslint-config-mantine/.prettierrc.js');

View File

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

View File

@ -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>,
];

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
import { Welcome } from './Welcome';
export default {
title: 'Welcome',
};
export const Usage = () => <Welcome />;

View File

@ -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/'
);
});
});

View File

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

View File

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

View File

@ -1,4 +0,0 @@
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

View File

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

View File

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

View File

@ -1,5 +0,0 @@
import { createTheme } from '@mantine/core';
export const theme = createTheme({
/** Put your mantine theme override here */
});

View File

@ -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');
/**
* 202401Date对象
*/
const get202401Date = (dayofMonth: number) => moment('202401', 'YYYYMM').date(dayofMonth).toDate();
/**
* 202401Date对象提取出是几号
*/
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,
};

View File

@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@ -1,5 +0,0 @@
import userEvent from '@testing-library/user-event';
export * from '@testing-library/react';
export { render } from './render';
export { userEvent };

View File

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

View File

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

View File

@ -1,9 +0,0 @@
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -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'),
},
},
});

File diff suppressed because it is too large Load Diff

2577
yarn.lock

File diff suppressed because it is too large Load Diff