This commit is contained in:
oneao committed 2026-01-30 17:22:20 +08:00
1 parent c62c9d080b
commit 6a8c31e20c
29 files changed
+1454 -357

No files matched your search

@@ -5,7 +5,7 @@ import { useCallback, useEffect, useState } from 'react'
import { Text, TouchableOpacity, View } from 'react-native'
import Animated, { useAnimatedRef } from 'react-native-reanimated'
import Sortable from 'react-native-sortables'
import { ADrawer, LucideIcon } from '@/components/common/ao'
import { ADivider, ADrawer, LucideIcon } from '@/components/common/ao'
import { useNavStore } from '@/store/navigation'
export default function TabsLayout() {
@@ -75,37 +75,42 @@ export default function TabsLayout() {
<View
className="
absolute
left-3 right-3 bottom-8
h-14
flex-row items-center
px-2
gap-4
bg-white
"
style={{ zIndex: 0 }}
style={{
zIndex: 0,
elevation: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: 100 },
}}
>
<View className="bg-white rounded-md shadow-lg h-12 w-12">
<TouchableOpacity className="w-full h-full items-center justify-center">
<LucideIcon icon="Bot" size={26} clickable={false} strokeWidth={2} />
</TouchableOpacity>
</View>
<TouchableOpacity className="w-12 items-center justify-center">
<LucideIcon icon="Bot" size={24} clickable={false} strokeWidth={2} />
</TouchableOpacity>
<View className="flex-1 bg-white rounded-md shadow-lg h-12">
<TouchableOpacity
className="w-full h-full items-center justify-center flex flex-row"
onPress={() => {
setShowNavDrawer(true)
}}
>
<LucideIcon icon={currentNav.icon} size={20} strokeWidth={2} />
<Text className="font-semibold text-lg ml-2">{currentNav.label}</Text>
</TouchableOpacity>
</View>
<View className="bg-white rounded-md shadow-lg h-12 w-12">
<TouchableOpacity className="w-full h-full items-center justify-center" onPress={refreshPage}>
<LucideIcon icon="RefreshCcw" size={26} clickable={false} strokeWidth={2} />
</TouchableOpacity>
</View>
<ADivider
direction="vertical"
/>
<TouchableOpacity
className="flex-1 items-center justify-center flex flex-row"
onPress={() => {
setShowNavDrawer(true)
}}
>
<LucideIcon icon={currentNav.icon} size={22} strokeWidth={2} />
<Text className="font-semibold text-lg ml-2">{currentNav.label}</Text>
</TouchableOpacity>
<ADivider direction="vertical" />
<TouchableOpacity className="w-12 items-center justify-center" onPress={refreshPage}>
<LucideIcon icon="RefreshCcw" size={24} clickable={false} strokeWidth={2} />
</TouchableOpacity>
</View>
<ADrawer
@@ -1,16 +1,11 @@
import type { MoneyRecord, MoneyRecordGroup } from '@/types/money/record'
import React, { useCallback, useState } from 'react'
import { RefreshControl, SectionList, Text, TouchableHighlight, Vibration, View } from 'react-native'
import {
Menu,
MenuOption,
MenuOptions,
MenuTrigger,
renderers,
} from 'react-native-popup-menu'
import { Pressable, RefreshControl, SectionList, Text, Vibration, View } from 'react-native'
import { LucideIcon } from '@/components/common/ao'
import EmptyState from '@/components/common/EmptyStatus'
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
import { Menu, MenuItem, MenuItemLabel } from '@/components/ui/menu'
import { useMoneyCategoryStore } from '@/store/moneyCategory'
import { useUserStore } from '@/store/user'
import { MoneyCategoryEnum } from '@/types/money/category'
@@ -124,124 +119,121 @@ export default function StaticTransactions({
const category = moneyCategoryStore.getCategoryById(item.category_id)
return (
<Menu
renderer={renderers.Popover}
rendererProps={{
placement: 'bottom',
preferredPlacement: 'bottom',
anchorStyle: { backgroundColor: 'transparent' },
closeAnimationDuration: 0,
style={{
elevation: 10,
borderWidth: 0,
shadowColor: '#000',
borderRadius: 4,
width: 160,
}}
onSelect={() => setSelectedId(null)}
onBackdropPress={() => setSelectedId(null)}
onOpen={() => setSelectedId(item.id)}
onClose={() => setSelectedId(null)}
>
<MenuTrigger
triggerOnLongPress
onPress={() => {
Vibration.vibrate(50)
}}
>
<View
className="flex-row items-center gap-4 py-2 px-5"
style={{
backgroundColor: item.id === selectedId ? '#f2f2f2' : 'transparent',
}}
>
<View className="relative w-10 h-10 rounded-full bg-gray-100 items-center justify-center">
<MoneyCategoryIcon name={category?.icon} size={24} color="#000" />
</View>
<View className="flex-1">
<Text className="text-gray-800 text-base">
{item.remark ? item.remark : category?.name}
</Text>
{
item.user_id !== userID && (
<Text className="text-gray-500 text-xs italic">
{item.username}
</Text>
)
}
</View>
<View>
<Text
className="text-sm text-gray-600 text-right"
offset={0}
placement="bottom"
onClose={() => {
setSelectedId(null)
}}
trigger={({ ...triggerProps }) => {
return (
<Pressable
{...triggerProps}
delayLongPress={300}
onLongPress={(e) => {
setSelectedId(item.id)
Vibration.vibrate(50)
// 手动触发菜单显示
triggerProps.onPress?.(e)
}}
onPress={(_e) => {
// 阻止点击触发,可以执行其他点击操作
// 如果需要点击执行其他操作,可以在这里添加
}}
>
<View
className="flex-row items-center gap-4 py-2 px-5"
style={{
backgroundColor: item.id === selectedId ? '#f2f2f2' : 'transparent',
}}
>
{category?.type === MoneyCategoryEnum.Income ? '+' : '-'}
{' '}
{item.amount}
</Text>
</View>
</View>
</MenuTrigger>
<MenuOptions
customStyles={{
optionsContainer: {
backgroundColor: 'white',
minWidth: 160,
},
<View className="relative w-10 h-10 rounded-full bg-gray-100 items-center justify-center">
<MoneyCategoryIcon name={category?.icon} size={24} color="#000" />
</View>
<View className="flex-1">
<Text className="text-gray-800 text-base">
{item.remark ? item.remark : category?.name}
</Text>
{
item.user_id !== userID && (
<Text className="text-gray-500 text-xs italic">
{item.username}
</Text>
)
}
</View>
<View>
<Text
className="text-sm text-gray-600 text-right"
>
{category?.type === MoneyCategoryEnum.Income ? '+' : '-'}
{' '}
{item.amount}
</Text>
</View>
</View>
</Pressable>
)
}}
>
{
userID === item.user_id && (
<MenuItem
key="update"
textValue="update"
className="flex gap-2"
style={{
paddingVertical: 10,
minWidth: 150,
}}
onPress={() => {
onUpdate?.(item)
}}
>
<LucideIcon icon="Pencil" size={18} />
<MenuItemLabel>更新</MenuItemLabel>
</MenuItem>
)
}
<MenuItem
key="delete"
textValue="delete"
className="flex gap-2"
style={{
paddingVertical: 10,
minWidth: 150,
}}
onPress={() => {
onDelete?.(item.id)
}}
>
{
userID === item.user_id && (
<>
<MenuOption
customStyles={{
OptionTouchableComponent: TouchableHighlight,
optionTouchable: {
underlayColor: '#f0f0f0',
activeOpacity: 0.7,
},
}}
onSelect={() => {
onUpdate?.(item)
}}
>
<View className="flex flex-row items-center h-10 px-2">
<LucideIcon icon="Pencil" />
<Text className="ml-2 text-lg">更新</Text>
</View>
</MenuOption>
<MenuOption
customStyles={{
OptionTouchableComponent: TouchableHighlight,
optionTouchable: {
underlayColor: '#f0f0f0',
activeOpacity: 0.7,
},
}}
onSelect={() => {
onDelete?.(item.id)
}}
>
<View className="flex flex-row items-center h-10 px-2">
<LucideIcon icon="Trash" />
<Text className="ml-2 text-lg">删除</Text>
</View>
</MenuOption>
</>
)
}
<MenuOption
customStyles={{
OptionTouchableComponent: TouchableHighlight,
optionTouchable: {
underlayColor: '#f0f0f0',
activeOpacity: 0.7,
},
}}
onSelect={() => {
onDetail?.(item.id)
}}
>
<View className="flex flex-row items-center h-10 px-2">
<LucideIcon icon="ReceiptText" />
<Text className="ml-2 text-lg">详细</Text>
</View>
</MenuOption>
</MenuOptions>
<LucideIcon icon="Trash" size={18} />
<MenuItemLabel>删除</MenuItemLabel>
</MenuItem>
<MenuItem
key="detail"
textValue="detail"
className="flex gap-2"
style={{
paddingVertical: 10,
minWidth: 150,
}}
onPress={() => {
onDetail?.(item.id)
}}
>
<LucideIcon icon="ReceiptText" size={18} />
<MenuItemLabel>详细</MenuItemLabel>
</MenuItem>
</Menu>
)
}}
@@ -161,7 +161,6 @@ export default function Money() {
onPress={() => {
router.navigate('/money/record/editor')
}}
className="mb-20"
>
<FabIcon as={AddIcon} size="xl" />
</Fab>
@@ -1,38 +1,131 @@
import type { AnalysisMoneyAmount } from '@/types/money/analysis'
import dayjs from 'dayjs'
import { useState } from 'react'
import { Text, TouchableOpacity, View } from 'react-native'
import { VictoryChart, VictoryLine, VictoryPie, VictoryTheme, VictoryTooltip, VictoryVoronoiContainer } from 'victory-native'
import { ADatePicker, ASegmentedTabs, LucideIcon } from '@/components/common/ao'
import { useEffect, useState } from 'react'
import { ScrollView, Text, TouchableOpacity, View } from 'react-native'
import Svg from 'react-native-svg'
import { VictoryChart, VictoryLabel, VictoryLine, VictoryPie, VictoryTheme, VictoryTooltip } from 'victory-native'
import { ADatePicker, ADateRangePicker, ASegmentedTabs, LucideIcon } from '@/components/common/ao'
import EmptyState from '@/components/common/EmptyStatus'
import CustomHeader from '@/components/custom/Header'
import PagerTabs from '@/components/custom/PagerTabs'
type GroupBy = 'day' | 'month' | 'year'
type AnalysisType = 'expense' | 'income'
import { analysisMoneyLineApi, analysisMoneyPieApi } from '@/request/api/money/analysis'
import { MoneyCategoryEnum } from '@/types/money/category'
import { getCurrentYear, getCurrentYearMonth, getEndOfMonth, getStartOfMonth } from '@/utils/time'
interface AnalysisContentProps {
analysisType: AnalysisType
analysisType: MoneyCategoryEnum
}
export default function Analysis() {
function AnalysisContent({ analysisType }: AnalysisContentProps) {
// 按月0 按年1 自定义2
const [timeType, setTimeType] = useState<number>(0)
const [groupBy, setGroupBy] = useState<GroupBy>('day')
const [startDate, setStartDate] = useState(
dayjs().format('YYYY-MM'),
)
const [endDate, setEndDate] = useState(
dayjs().format('YYYY-MM'),
)
// 开始结束时间
const [startDate, setStartDate] = useState(getCurrentYearMonth())
const [endDate, setEndDate] = useState('')
// 显示
const [showDatePicker, setShowDatePicker] = useState(false)
// 数据
const [lineRecords, setLineRecords] = useState<AnalysisMoneyAmount[]>([])
const [pieRecords, setPieRecords] = useState<AnalysisMoneyAmount[]>([])
// 请求
function fetchChartsData() {
let start = ''
let end = ''
const selectedStartDate = dayjs(startDate)
const selectedEndDate = dayjs(endDate)
let periodType = 0
if (timeType === 0) {
start = selectedStartDate.startOf('month').format('YYYY-MM-DD')
end = selectedStartDate.endOf('month').format('YYYY-MM-DD')
periodType = 0
}
else if (timeType === 1) {
start = selectedStartDate.startOf('year').format('YYYY-MM-DD')
end = selectedStartDate.endOf('year').format('YYYY-MM-DD')
periodType = 1
}
else {
console.log(selectedStartDate.isSame(selectedEndDate, 'day'))
console.log(selectedStartDate.isSame(selectedEndDate, 'month'))
console.log(selectedStartDate.isSame(selectedEndDate, 'year'))
if (selectedStartDate.isSame(selectedEndDate, 'month')) {
periodType = 0
start = selectedStartDate.startOf('month').format('YYYY-MM-DD')
end = selectedEndDate.endOf('month').format('YYYY-MM-DD')
}
else if (selectedStartDate.isSame(selectedEndDate, 'year')) {
periodType = 1
start = selectedStartDate.startOf('year').format('YYYY-MM-DD')
end = selectedEndDate.endOf('year').format('YYYY-MM-DD')
}
else {
periodType = 2
start = selectedStartDate.format('YYYY-MM-DD')
end = selectedEndDate.format('YYYY-MM-DD')
}
}
analysisMoneyLineApi({
start_date: start,
end_date: end,
type: analysisType,
period_type: periodType,
}).then((res) => {
if (res.isSuccess) {
setLineRecords(res.data.records)
}
})
analysisMoneyPieApi({
start_date: start,
end_date: end,
type: analysisType,
period_type: periodType,
}).then((res) => {
if (res.isSuccess) {
setPieRecords(res.data.records)
}
})
}
useEffect(() => {
fetchChartsData()
}, [timeType, startDate, endDate])
return (
<View className="p-4">
<ScrollView
className="p-4"
>
<ASegmentedTabs
value={timeType}
onChange={(index) => {
setTimeType(index)
if (index === 2) {
// 自定义类型,本月的 1号 - 终止号
setStartDate(getStartOfMonth())
setEndDate(getEndOfMonth())
}
else if (index === 1) {
// 按年
setStartDate(getCurrentYear())
setEndDate('')
}
else {
// 按月
setStartDate(getCurrentYearMonth())
setEndDate('')
}
}}
tabs={[
{ key: '1', label: '按月' },
@@ -42,9 +135,9 @@ export default function Analysis() {
/>
<TouchableOpacity
className="mt-14 flex flex-row justify-center items-center"
className="flex flex-row justify-center items-center"
>
<LucideIcon icon="Calendar" size={16} color="#666" clickable={false} />
<LucideIcon icon="CalendarDays" size={14} color="#666" clickable={false} />
<Text className="font-semibold text-lg mr-2 ml-1">{startDate}</Text>
{timeType === 2 && (
<>
@@ -63,40 +156,99 @@ export default function Analysis() {
<VictoryChart
height={220}
theme={VictoryTheme.material}
padding={{ top: 15, bottom: 30, left: 40, right: 40 }}
>
<VictoryLine
interpolation="catmullRom"
data={[
]}
data={lineRecords}
animate={{
duration: 1000,
}}
/>
</VictoryChart>
</View>
<View className="mt-4">
<View
className="mt-4"
>
<Text className="text-lg font-semibold text-gray-700">分类构成</Text>
<VictoryPie
height={250}
innerRadius={50}
data={[
]}
theme={VictoryTheme.material}
/>
{
pieRecords.length > 0
? (
<VictoryPie
height={250}
animate={{ duration: 1000 }}
data={pieRecords}
theme={VictoryTheme.material}
/>
)
: (
<EmptyState title="空" />
)
}
</View>
<ADatePicker
show={showDatePicker}
mode="month"
onConfirm={(val) => {
setShowDatePicker(false)
}}
onClose={() => {
setShowDatePicker(false)
}}
/>
</View>
<View className="mt-4" style={{ paddingBottom: 24 }}>
<Text className="text-lg font-semibold text-gray-700">分类详细</Text>
{
pieRecords.length > 0
? (
<View className="mt-4 space-y-3">
{pieRecords.map((item, index) => (
<View
key={index}
className={`flex flex-row justify-between items-center p-3 bg-white shadow-md rounded-lg ${index !== pieRecords.length - 1 ? 'border-b border-gray-300' : ''}`}
>
<Text className="text-base font-medium text-gray-800">{item.x}</Text>
<Text className="text-base font-semibold text-gray-700">
{item.y}
{' '}
元
</Text>
</View>
))}
</View>
)
: (
<EmptyState title="空" />
)
}
</View>
{
timeType === 2
? (
<ADateRangePicker
show={showDatePicker}
onConfirm={(startDate, endDate) => {
setStartDate(startDate)
setEndDate(endDate)
setShowDatePicker(false)
}}
onClose={() => {
setShowDatePicker(false)
}}
/>
)
: (
<ADatePicker
show={showDatePicker}
mode={timeType === 0 ? 'month' : 'year'}
onConfirm={(val) => {
setStartDate(val)
setShowDatePicker(false)
}}
onClose={() => {
setShowDatePicker(false)
}}
/>
)
}
</ScrollView>
)
}
@@ -112,12 +264,12 @@ export default function Analysis() {
{
key: 'expense',
title: '支出',
element: <AnalysisContent analysisType="expense" />,
element: <AnalysisContent analysisType={MoneyCategoryEnum.Expense} />,
},
{
key: 'income',
title: '收入',
element: <AnalysisContent analysisType="income" />,
element: <AnalysisContent analysisType={MoneyCategoryEnum.Income} />,
},
]}
/>
@@ -0,0 +1,89 @@
import type { IDateRangePickerProps } from '@yz1311/react-native-wheel-picker'
import { DateRangePicker } from '@yz1311/react-native-wheel-picker'
import dayjs from 'dayjs'
import { useEffect, useRef, useState } from 'react'
import { Animated, Modal, StyleSheet, TouchableOpacity } from 'react-native'
interface ADateRangePickerProps extends IDateRangePickerProps {
show?: boolean
onClose?: () => void
onConfirm?: (startDate: string, endDate: string) => void
}
export default function ADateRangePicker({
show,
onClose,
onConfirm,
...restProps
}: ADateRangePickerProps) {
const fadeAnim = useRef(new Animated.Value(0)).current
const slideAnim = useRef(new Animated.Value(300)).current
const [visible, setVisible] = useState(show)
useEffect(() => {
if (show) {
setVisible(true)
fadeAnim.setValue(0)
slideAnim.setValue(300)
Animated.parallel([
Animated.timing(fadeAnim, { toValue: 1, duration: 250, useNativeDriver: true }),
Animated.timing(slideAnim, { toValue: 0, duration: 250, useNativeDriver: true }),
]).start()
}
else {
Animated.parallel([
Animated.timing(fadeAnim, { toValue: 0, duration: 250, useNativeDriver: true }),
Animated.timing(slideAnim, { toValue: 280, duration: 250, useNativeDriver: true }),
]).start(() => setVisible(false))
}
}, [show])
const handleCancel = () => onClose?.()
const handleConfirm = (startDate: Date, endDate: Date) => {
const formattedStartDate = dayjs(startDate).format('YYYY-MM-DD')
const formattedEndDate = dayjs(endDate).format('YYYY-MM-DD')
onConfirm?.(formattedStartDate, formattedEndDate)
}
if (!visible)
return null
return (
<Modal transparent visible={visible} animationType="none">
<Animated.View style={[styles.overlay, { opacity: fadeAnim }]}>
<TouchableOpacity style={{ flex: 1 }} activeOpacity={1} onPress={handleCancel} />
</Animated.View>
<Animated.View
style={[styles.pickerContainer, { transform: [{ translateY: slideAnim }] }]}
>
<DateRangePicker
pickerTitle="时间范围"
pickerCancelBtnText="取消"
pickerConfirmBtnText="确认"
{...restProps}
isModal={false}
onPickerCancel={handleCancel}
onPickerConfirm={handleConfirm}
/>
</Animated.View>
</Modal>
)
}
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.5)',
},
pickerContainer: {
backgroundColor: '#fff',
overflow: 'hidden',
position: 'absolute',
bottom: 0,
width: '100%',
},
})
@@ -2,8 +2,9 @@ import { Avatar } from './Avatar'
import Button from './Button'
import { Cell, CellGroup } from './Cell'
import Checkbox from './Checkbox'
import DatePicker from './DatePicker'
import DatePicker from './DatePicker'
import DateRangePicker from './DateRangePicker'
import Divider from './Divider'
import Drawer from './Drawer'
import Form, { FormRef } from './Form'
@@ -13,7 +14,6 @@ import ListItem from './ListItem'
import LucideIcon from './LucideIcon'
import Modal from './Modal'
import SegmentedTabs from './SegmentedTabs'
import Tab from './Tab'
export {
@@ -23,6 +23,7 @@ export {
CellGroup as ACellGroup,
Checkbox as ACheckbox,
DatePicker as ADatePicker,
DateRangePicker as ADateRangePicker,
Divider as ADivider,
Drawer as ADrawer,
Form as AForm,
@@ -0,0 +1,13 @@
import type { AnalysisMoneyLineResult, AnalysisMoneyParams, AnalysisMoneyPieResult } from '@/types/money/analysis'
import alovaInstance from '../../index'
const API_PREFIX = '/money/analysis'
export function analysisMoneyLineApi(params: AnalysisMoneyParams) {
return alovaInstance.Post<ResponseData<AnalysisMoneyLineResult>>(`${API_PREFIX}/line`, params)
}
export function analysisMoneyPieApi(params: AnalysisMoneyParams) {
return alovaInstance.Post<ResponseData<AnalysisMoneyPieResult>>(`${API_PREFIX}/pie`, params)
}
+2 -2
View File
@@ -28,8 +28,8 @@ function isWhiteListed(url: string) {
}
const alovaInstance = createAlova({
baseURL: 'http://192.168.5.6:8086/app',
// baseURL: 'http://192.168.1.137:8086/app',
// baseURL: 'http://192.168.5.6:8086/app',
baseURL: 'http://192.168.1.137:8086/app',
requestAdapter: axiosRequestAdapter(),
timeout: 5000,
cacheFor: null,
@@ -0,0 +1,19 @@
export interface AnalysisMoneyParams {
start_date: string
end_date: string
type: number
period_type: number // 0 = 日分析, 1 = 月分析, 2 = 年分析
}
export interface AnalysisMoneyAmount {
x: string
y: string
}
export interface AnalysisMoneyLineResult {
records: AnalysisMoneyAmount[]
}
export interface AnalysisMoneyPieResult {
records: AnalysisMoneyAmount[]
}
+14
View File
@@ -28,6 +28,10 @@ export function getCurrentYearMonth(format = 'YYYY-MM'): string {
return dayjs().format(format)
}
export function getCurrentYear(): string {
return dayjs().format('YYYY')
}
/** 字符串 / 数字 / Date → Date(无效返回 null) */
export function toDate(input: string | number | Date | undefined | null): Date {
if (!input)
@@ -105,3 +109,13 @@ export function formatRelativeTime(date: string | Date | number): string {
return `${years}年前`
}
}
// 获取本月的第一天
export function getStartOfMonth() {
return dayjs().startOf('month').format('YYYY-MM-DD')
}
// 获取本月的最后一天
export function getEndOfMonth() {
return dayjs().endOf('month').format('YYYY-MM-DD')
}
+80 -73
View File
@@ -1,87 +1,94 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>财务分析 Demo</title>
<script src="https://cdn.tailwindcss.com"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Test</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
#output {
margin-top: 20px;
padding: 10px;
border: 1px solid #ccc;
min-height: 150px;
}
#sendMessage {
margin-top: 20px;
padding: 10px;
width: 100%;
}
</style>
</head>
<body class="bg-gray-100 p-4">
<body>
<!-- Header: 收支切换 -->
<div class="flex justify-center gap-4 mb-4">
<button id="expenseBtn" class="px-4 py-2 rounded-full bg-blue-500 text-white font-semibold">支出</button>
<button id="incomeBtn" class="px-4 py-2 rounded-full bg-gray-200 text-gray-700 font-semibold">收入</button>
</div>
<!-- 时间维度选择 -->
<div class="flex justify-center gap-2 mb-4">
<button class="timeBtn px-3 py-1 rounded-full bg-blue-500 text-white font-medium" data-type="month">按月</button>
<button class="timeBtn px-3 py-1 rounded-full bg-gray-200 text-gray-700 font-medium" data-type="year">按年</button>
<button class="timeBtn px-3 py-1 rounded-full bg-gray-200 text-gray-700 font-medium" data-type="custom">自定义</button>
</div>
<!-- 日期选择 -->
<div id="datePickerContainer" class="flex justify-center mb-4">
<!-- 默认显示按月日期选择 -->
<input type="month" id="monthInput" class="p-2 rounded border border-gray-300">
<input type="month" id="yearInput" class="p-2 rounded border border-gray-300 hidden">
<div id="customInput" class="flex gap-2 hidden">
<input type="date" id="startDate" class="p-2 rounded border border-gray-300">
<span class="self-center">~</span>
<input type="date" id="endDate" class="p-2 rounded border border-gray-300">
<h2>WebSocket Test</h2>
<div>
<label for="messageInput">Enter Message:</label>
<input type="text" id="messageInput" placeholder="Type your message here">
<button id="sendBtn">Send Message</button>
</div>
</div>
<!-- 示例图表区域 -->
<div class="bg-white p-4 rounded shadow text-center">
<p class="text-gray-500">这里显示图表或分析结果</p>
</div>
<div id="output">
<h3>Messages:</h3>
<div id="messages"></div>
</div>
<script>
const expenseBtn = document.getElementById('expenseBtn')
const incomeBtn = document.getElementById('incomeBtn')
const timeBtns = document.querySelectorAll('.timeBtn')
const monthInput = document.getElementById('monthInput')
const yearInput = document.getElementById('yearInput')
const customInput = document.getElementById('customInput')
<script>
// Create a new WebSocket connection
const socket = new WebSocket('ws://192.168.1.137:8086/app/ws');
// 收支切换逻辑
expenseBtn.addEventListener('click', () => {
expenseBtn.classList.add('bg-blue-500', 'text-white')
expenseBtn.classList.remove('bg-gray-200', 'text-gray-700')
incomeBtn.classList.add('bg-gray-200', 'text-gray-700')
incomeBtn.classList.remove('bg-blue-500', 'text-white')
})
// Get HTML elements
const sendButton = document.getElementById('sendBtn');
const messageInput = document.getElementById('messageInput');
const messagesDiv = document.getElementById('messages');
incomeBtn.addEventListener('click', () => {
incomeBtn.classList.add('bg-blue-500', 'text-white')
incomeBtn.classList.remove('bg-gray-200', 'text-gray-700')
expenseBtn.classList.add('bg-gray-200', 'text-gray-700')
expenseBtn.classList.remove('bg-blue-500', 'text-white')
})
// WebSocket onopen event: Fired when WebSocket connection is established
socket.onopen = function(event) {
console.log("Connected to WebSocket server!");
displayMessage("Connected to WebSocket server!");
};
// 时间维度切换逻辑
timeBtns.forEach(btn => {
btn.addEventListener('click', () => {
timeBtns.forEach(b => {
b.classList.remove('bg-blue-500', 'text-white')
b.classList.add('bg-gray-200', 'text-gray-700')
})
btn.classList.add('bg-blue-500', 'text-white')
btn.classList.remove('bg-gray-200', 'text-gray-700')
// WebSocket onmessage event: Fired when a message is received from the server
socket.onmessage = function(event) {
console.log("Received message from server: " + event.data);
displayMessage("Server: " + event.data);
};
// 显示对应日期选择控件
const type = btn.dataset.type
monthInput.classList.add('hidden')
yearInput.classList.add('hidden')
customInput.classList.add('hidden')
// WebSocket onerror event: Fired when there's an error with the WebSocket
socket.onerror = function(error) {
console.log(error)
console.error("WebSocket Error: " + error);
displayMessage("WebSocket Error: " + error);
};
if (type === 'month') monthInput.classList.remove('hidden')
if (type === 'year') yearInput.classList.remove('hidden')
if (type === 'custom') customInput.classList.remove('hidden')
})
})
</script>
// WebSocket onclose event: Fired when the connection is closed
socket.onclose = function(event) {
console.log("WebSocket connection closed");
displayMessage("WebSocket connection closed");
};
// Function to display messages on the page
function displayMessage(message) {
const messageElement = document.createElement('div');
messageElement.textContent = message;
messagesDiv.appendChild(messageElement);
}
// Button to send the message
sendButton.onclick = function() {
const message = messageInput.value;
if (message) {
console.log("Sending message: " + message);
socket.send(message);
displayMessage("You: " + message);
messageInput.value = ''; // Clear the input field
} else {
alert("Please enter a message!");
}
};
</script>
</body>
</html>