feat: 大致完成网页UI
This commit is contained in:
parent
c7fcd0d072
commit
56391a68f7
12
README.md
12
README.md
@ -21,8 +21,18 @@
|
||||
|
||||
## 零碎TODO
|
||||
|
||||
[ ] 精细化模板卡片的返回值,以及存储
|
||||
|
||||
[ ] 模板改成JSON格式的,模板ID的话只能自己创建
|
||||
|
||||
[ ] 支持编辑更多设置,飞书网页就行
|
||||
|
||||
[ ] 飞书网页鉴权逻辑,分为内网和外网部分,内网使用米盾,支持查看全部的提醒信息等等,外网只支持创建/编辑卡片带出来的提醒,但是这里不支持鉴权,尝试改成群里的聊天仅对发送者可见
|
||||
|
||||
[ ] 仅对发送者可见的卡片是否可以后续修改成结果对全员显示
|
||||
|
||||
[ ] 提醒挂靠在卡片发送者的身上,不论网页还是卡片,谁触发提醒卡片,挂靠在谁身上
|
||||
|
||||
[ ] 支持快速提醒,输入数字,[1-120]分钟
|
||||
|
||||
[ ] 销毁创建用卡片,创建卡片添加取消按钮,点击去掉卡片可交互部分
|
||||
@ -39,6 +49,8 @@
|
||||
|
||||
[ ] 离职人员的提醒会发一条通知,然后提醒别人认领
|
||||
|
||||
[x] 数据库新增图片key value以及原图保存,方便查看
|
||||
|
||||
[x] 支持同一提醒设置多个时间
|
||||
|
||||
[x] 每个时间,支持多时间点设置,类似每周一二三,每月1 3 10号
|
||||
|
@ -9,7 +9,11 @@
|
||||
"settings": {
|
||||
"files.autoSave": "off",
|
||||
"editor.guides.bracketPairs": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
},
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
@ -26,5 +30,5 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "bash -i /workspaces/view/.devcontainer/initial.bash"
|
||||
"postCreateCommand": "bash -i /workspaces/egg_server/view/.devcontainer/initial.bash"
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
echo "alias dev=\"cd /workspaces/view && yarn dev\"" >> /home/node/.bashrc
|
||||
echo "alias dev=\"cd /workspaces/egg_server/view && yarn dev\"" >> /home/node/.bashrc
|
@ -4,6 +4,8 @@ module.exports = {
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
rules: {
|
||||
'import/extensions': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'no-console': 'off',
|
||||
},
|
||||
};
|
||||
|
@ -1,34 +0,0 @@
|
||||
# Mantine Vite template
|
||||
|
||||
## Features
|
||||
|
||||
This template comes with the following features:
|
||||
|
||||
- [PostCSS](https://postcss.org/) with [mantine-postcss-preset](https://mantine.dev/styles/postcss-preset)
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Storybook](https://storybook.js.org/)
|
||||
- [Jest](https://jestjs.io/) setup with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro)
|
||||
- ESLint setup with [eslint-config-mantine](https://github.com/mantinedev/eslint-config-mantine)
|
||||
|
||||
## npm scripts
|
||||
|
||||
## Build and dev scripts
|
||||
|
||||
- `dev` – start development server
|
||||
- `build` – build production version of the app
|
||||
- `preview` – locally preview production build
|
||||
|
||||
### Testing scripts
|
||||
|
||||
- `typecheck` – checks TypeScript types
|
||||
- `lint` – runs ESLint
|
||||
- `prettier:check` – checks files with Prettier
|
||||
- `jest` – runs jest tests
|
||||
- `jest:watch` – starts jest watch
|
||||
- `test` – runs `jest`, `prettier:check`, `lint` and `typecheck` scripts
|
||||
|
||||
### Other scripts
|
||||
|
||||
- `storybook` – starts storybook dev server
|
||||
- `storybook:build` – build production storybook bundle to `storybook-static`
|
||||
- `prettier:write` – formats all files with Prettier
|
@ -35,9 +35,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"
|
||||
"react-router-dom": "^6.11.2",
|
||||
"pocketbase": "^0.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^7.0.18",
|
||||
|
@ -1,11 +1,15 @@
|
||||
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>
|
||||
);
|
||||
|
302
view/src/components/MultiTimePicker/index.tsx
Normal file
302
view/src/components/MultiTimePicker/index.tsx
Normal file
@ -0,0 +1,302 @@
|
||||
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;
|
27
view/src/components/MultiTimePicker/style.module.css
Normal file
27
view/src/components/MultiTimePicker/style.module.css
Normal file
@ -0,0 +1,27 @@
|
||||
.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,6 +1,271 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
25
view/src/server/index.ts
Normal file
25
view/src/server/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
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 };
|
42
view/src/utils/time.ts
Normal file
42
view/src/utils/time.ts
Normal file
@ -0,0 +1,42 @@
|
||||
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,
|
||||
};
|
@ -2,7 +2,8 @@
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node"
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
@ -1,7 +1,14 @@
|
||||
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'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -7677,6 +7677,11 @@ mkdirp@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
moment@^2.29.4:
|
||||
version "2.29.4"
|
||||
resolved "https://pkgs.d.xiaomi.net:443/artifactory/api/npm/mi-npm/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||
integrity sha1-Pb4FKIn+fBsu2Wb8s6dzKJZO8Qg=
|
||||
|
||||
mri@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
||||
@ -8129,6 +8134,11 @@ pkg-dir@^5.0.0:
|
||||
dependencies:
|
||||
find-up "^5.0.0"
|
||||
|
||||
pocketbase@^0.16.0:
|
||||
version "0.16.0"
|
||||
resolved "https://pkgs.d.xiaomi.net:443/artifactory/api/npm/mi-npm/pocketbase/-/pocketbase-0.16.0.tgz#156c05a26b126b97a202b7173e9c1544f090eff1"
|
||||
integrity sha1-FWwFomsSa5eiArcXPpwVRPCQ7/E=
|
||||
|
||||
polished@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/polished/-/polished-4.2.2.tgz#2529bb7c3198945373c52e34618c8fe7b1aa84d1"
|
||||
|
Loading…
x
Reference in New Issue
Block a user