u
This commit is contained in:
1 parent
bb780a42ab
commit
d3145e3dc5
19 files changed
+15361
-413
No files matched your search
@@ -1,11 +1,11 @@
|
||||
import type { SortableGridRenderItem } from 'react-native-sortables'
|
||||
import type { MoneyCategoryItem } from '@/types/money/category'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Platform, Text, Vibration, View } from 'react-native'
|
||||
import { Pressable, Text, Vibration, View } from 'react-native'
|
||||
|
||||
import Animated, { useAnimatedRef } from 'react-native-reanimated'
|
||||
import Sortable, { DragStartParams } from 'react-native-sortables'
|
||||
import { AButton, ASegmentedTabs, LucideIcon } from '@/components/common/ao'
|
||||
import Sortable from 'react-native-sortables'
|
||||
import { AButton, ATab, LucideIcon } from '@/components/common/ao'
|
||||
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||
@@ -27,7 +27,10 @@ export default function Example() {
|
||||
|
||||
const renderItem = useCallback<SortableGridRenderItem<MoneyCategoryItem>>(
|
||||
({ item }) => (
|
||||
<View
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
console.log('点击进入编辑')
|
||||
}}
|
||||
className="flex flex-col items-center justify-center"
|
||||
>
|
||||
<View
|
||||
@@ -49,7 +52,7 @@ export default function Example() {
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
),
|
||||
[],
|
||||
)
|
||||
@@ -62,42 +65,40 @@ export default function Example() {
|
||||
|
||||
<View className="flex-1">
|
||||
<View className="pt-2 mb-4 px-3 flex flex-row items-center justify-between">
|
||||
<ASegmentedTabs
|
||||
<ATab
|
||||
defaultIndex={selectedTab}
|
||||
onChange={(index) => {
|
||||
setSelectedTab(index)
|
||||
}}
|
||||
tabs={[
|
||||
data={[
|
||||
{
|
||||
key: 'income',
|
||||
label: '收入',
|
||||
},
|
||||
{
|
||||
key: 'expense',
|
||||
label: '支出',
|
||||
},
|
||||
]}
|
||||
style={{
|
||||
width: 140,
|
||||
}}
|
||||
/>
|
||||
|
||||
<View className="flex flex-row gap-2">
|
||||
|
||||
<AButton
|
||||
className="rounded-full"
|
||||
leftIcon={{
|
||||
icon: 'Plus',
|
||||
className: 'mr-0',
|
||||
color: '#fff',
|
||||
clickable: false,
|
||||
size: 18,
|
||||
}}
|
||||
>
|
||||
添加分类
|
||||
新增分类
|
||||
</AButton>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
|
||||
<View className="flex-1 px-3">
|
||||
<View className="flex-1">
|
||||
<Sortable.MultiZoneProvider>
|
||||
<Sortable.PortalProvider enabled>
|
||||
<Animated.ScrollView
|
||||
@@ -116,8 +117,14 @@ export default function Example() {
|
||||
Vibration.vibrate(50)
|
||||
setDraggingId(data.key)
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
onDragEnd={(data) => {
|
||||
setDraggingId('')
|
||||
if (draggingDeleteZone)
|
||||
return
|
||||
|
||||
const newOrder = data.data
|
||||
|
||||
moneyCategoryStore.updateCategories(newOrder, selectedTab)
|
||||
}}
|
||||
/>
|
||||
</Animated.ScrollView>
|
||||
@@ -132,7 +139,11 @@ export default function Example() {
|
||||
|
||||
// 设置 1 秒后删除
|
||||
const timer = setTimeout(() => {
|
||||
setCategories(prev => prev.filter(c => c.id !== Number(draggingId)))
|
||||
const deleteId = Number(draggingId)
|
||||
setCategories(prev => prev.filter(c => c.id !== deleteId))
|
||||
|
||||
moneyCategoryStore.deleteCategoryById(deleteId, selectedTab)
|
||||
|
||||
setDraggingId('')
|
||||
setDraggingDeleteZone(false)
|
||||
setDeleteTimer(null)
|
||||
@@ -157,7 +168,7 @@ export default function Example() {
|
||||
minActivationDistance={40}
|
||||
>
|
||||
<View
|
||||
className={`flex flex-row items-center justify-center h-12 rounded-md gap-2 ${
|
||||
className={`flex flex-row items-center justify-center h-14 gap-2 ${
|
||||
draggingDeleteZone ? 'bg-red-600' : 'bg-red-400'
|
||||
}`}
|
||||
>
|
||||
@@ -166,38 +177,7 @@ export default function Example() {
|
||||
拖到这里删除
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
</Sortable.BaseZone>
|
||||
|
||||
<View className="flex flex-row mb-8 mt-4 w-full gap-4">
|
||||
<AButton
|
||||
className="flex-1"
|
||||
variant="outline"
|
||||
leftIcon={{
|
||||
icon: 'RefreshCcw',
|
||||
className: 'mr-0',
|
||||
color: '#333',
|
||||
clickable: false,
|
||||
size: 18,
|
||||
}}
|
||||
>
|
||||
恢复
|
||||
</AButton>
|
||||
|
||||
<AButton
|
||||
className="flex-1"
|
||||
leftIcon={{
|
||||
icon: 'Save',
|
||||
className: 'mr-0',
|
||||
color: '#fff',
|
||||
clickable: false,
|
||||
size: 18,
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</AButton>
|
||||
</View>
|
||||
|
||||
</Sortable.PortalProvider>
|
||||
</Sortable.MultiZoneProvider>
|
||||
</View>
|
||||
|
||||
@@ -12,7 +12,7 @@ import LucideIcon from './LucideIcon'
|
||||
type IconProp = (LucideIconProps & { icon: keyof typeof icons }) | React.ReactNode
|
||||
|
||||
interface CustomButtonProps {
|
||||
children: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
onPress?: () => void
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
@@ -40,19 +40,21 @@ const Button: React.FC<CustomButtonProps> = ({
|
||||
rightIcon,
|
||||
}) => {
|
||||
const isDisabled = disabled || loading
|
||||
|
||||
const hasChildren = !!children
|
||||
console.log(hasChildren)
|
||||
const renderLeftIcon = () => {
|
||||
if (loading)
|
||||
return <ButtonSpinner color="#fff" className="mr-2" />
|
||||
return <ButtonSpinner color="#fff" className={hasChildren ? 'mr-2' : ''} />
|
||||
|
||||
if (!leftIcon)
|
||||
return null
|
||||
|
||||
if (React.isValidElement(leftIcon)) {
|
||||
return <span className="mr-2">{leftIcon}</span>
|
||||
return <span className={hasChildren ? 'mr-2' : ''}>{leftIcon}</span>
|
||||
}
|
||||
|
||||
const { icon, ...props } = leftIcon as LucideIconProps & { icon: keyof typeof icons }
|
||||
return <LucideIcon icon={icon} className="mr-2" {...props} />
|
||||
return <LucideIcon icon={icon} className={hasChildren ? 'mr-2' : ''} {...props} />
|
||||
}
|
||||
|
||||
const renderRightIcon = () => {
|
||||
@@ -60,11 +62,11 @@ const Button: React.FC<CustomButtonProps> = ({
|
||||
return null
|
||||
|
||||
if (React.isValidElement(rightIcon)) {
|
||||
return <span className="ml-2">{rightIcon}</span>
|
||||
return <span className={hasChildren ? 'ml-2' : ''}>{rightIcon}</span>
|
||||
}
|
||||
|
||||
const { icon, ...props } = rightIcon as LucideIconProps & { icon: keyof typeof icons }
|
||||
return <LucideIcon icon={icon} className="ml-2" {...props} />
|
||||
return <LucideIcon icon={icon} className={hasChildren ? 'ml-2' : ''} {...props} />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -78,7 +80,11 @@ const Button: React.FC<CustomButtonProps> = ({
|
||||
size={size}
|
||||
>
|
||||
{renderLeftIcon()}
|
||||
<ButtonText>{children}</ButtonText>
|
||||
{
|
||||
children && (
|
||||
<ButtonText>{children}</ButtonText>
|
||||
)
|
||||
}
|
||||
{renderRightIcon()}
|
||||
</NativeButton>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useState } from 'react'
|
||||
import { Pressable, Text, View } from 'react-native'
|
||||
|
||||
interface TabItem {
|
||||
label: string
|
||||
key: string
|
||||
}
|
||||
|
||||
interface TabProps {
|
||||
data?: TabItem[]
|
||||
defaultIndex?: number
|
||||
onChange?: (index: number) => void
|
||||
}
|
||||
|
||||
export default function Tab({ data, defaultIndex = 0, onChange }: TabProps) {
|
||||
const [selectedIndex, setSelectedIndex] = useState(defaultIndex)
|
||||
|
||||
if (!data || data.length === 0)
|
||||
return null
|
||||
|
||||
const handlePress = (index: number) => {
|
||||
setSelectedIndex(index)
|
||||
onChange?.(index)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex flex-row gap-2 overflow-hidden">
|
||||
{data.map((item, index) => {
|
||||
const isActive = index === selectedIndex
|
||||
return (
|
||||
<Pressable
|
||||
key={item.key}
|
||||
onPress={() => handlePress(index)}
|
||||
className={`items-center justify-center rounded-md px-4 py-2 ${
|
||||
isActive ? 'bg-gray-800' : 'bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Text className={`${isActive ? 'text-white' : 'text-gray-700'}`}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import Input from './Input'
|
||||
import LucideIcon from './LucideIcon'
|
||||
import Modal from './Modal'
|
||||
import SegmentedTabs from './SegmentedTabs'
|
||||
import Tab from './Tab'
|
||||
|
||||
export {
|
||||
Button as AButton,
|
||||
@@ -21,5 +22,6 @@ export {
|
||||
Input as AInput,
|
||||
Modal as AModal,
|
||||
SegmentedTabs as ASegmentedTabs,
|
||||
Tab as ATab,
|
||||
LucideIcon,
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
import type { QueryMoneyCategoryResult } from '@/types/money/category'
|
||||
import type { DeleteMoneyCategoryParams, QueryMoneyCategoryResult, UpdateMoneyCategorySortParams } from '@/types/money/category'
|
||||
import alovaInstance from '../../index'
|
||||
|
||||
const API_PREFIX = '/money'
|
||||
const API_PREFIX = '/money/category'
|
||||
|
||||
export function queryMoneyCategoryApi() {
|
||||
return alovaInstance.Get<ResponseData<QueryMoneyCategoryResult>>(`${API_PREFIX}/category`)
|
||||
return alovaInstance.Get<ResponseData<QueryMoneyCategoryResult>>(`${API_PREFIX}`)
|
||||
}
|
||||
|
||||
export function deleteMoneyCategoryApi(params: DeleteMoneyCategoryParams) {
|
||||
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/delete`, params)
|
||||
}
|
||||
|
||||
export function updateMoneyCategorySortApi(params: UpdateMoneyCategorySortParams) {
|
||||
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/updateSort`, params)
|
||||
}
|
||||
@@ -30,8 +30,8 @@ function isWhiteListed(url: string) {
|
||||
}
|
||||
|
||||
const alovaInstance = createAlova({
|
||||
baseURL: 'http://192.168.5.6:8086/app',
|
||||
// baseURL: 'http://192.168.1.136:8085/app',
|
||||
// baseURL: 'http://192.168.5.6:8086/app',
|
||||
baseURL: 'http://192.168.1.136:8086/app',
|
||||
requestAdapter: axiosRequestAdapter(),
|
||||
timeout: 5000,
|
||||
cacheFor: null,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { MoneyCategoryItem } from '@/types/money/category'
|
||||
import type { MoneyCategoryItem, UpdateMoneyCategorySortItem } from '@/types/money/category'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { create } from 'zustand'
|
||||
import { createJSONStorage, persist } from 'zustand/middleware'
|
||||
import { queryMoneyCategoryApi } from '@/request/api/money/category'
|
||||
import { deleteMoneyCategoryApi, queryMoneyCategoryApi, updateMoneyCategorySortApi } from '@/request/api/money/category'
|
||||
|
||||
interface MoneyCategoryStore {
|
||||
categories: MoneyCategoryItem[]
|
||||
@@ -16,6 +16,9 @@ interface MoneyCategoryStore {
|
||||
getIncomeCategories: () => MoneyCategoryItem[]
|
||||
|
||||
getCategoryById: (id: number) => MoneyCategoryItem | null
|
||||
|
||||
updateCategories: (data: MoneyCategoryItem[], type: number) => void
|
||||
deleteCategoryById: (id: number, type: number) => void
|
||||
}
|
||||
|
||||
export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||
@@ -55,6 +58,72 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||
getCategoryById: (id: number) => {
|
||||
return get().categories.find(c => c.id === id) || null
|
||||
},
|
||||
updateCategories: (data, type) => {
|
||||
// 获取当前所有分类
|
||||
const allCategories = get().categories
|
||||
|
||||
// 保留另一类分类
|
||||
const otherCategories = allCategories.filter(c => c.type !== type)
|
||||
|
||||
// 给当前类型的数据重新计算 sort_number
|
||||
const updatedData = data.map((item, index) => ({
|
||||
...item,
|
||||
sort_number: index, // 按顺序重新赋值
|
||||
}))
|
||||
|
||||
// 合并另一类分类
|
||||
const finalCategories = [...otherCategories, ...updatedData]
|
||||
|
||||
// 构造后端请求 payload,只需要 id 和 sort_number
|
||||
const payload: UpdateMoneyCategorySortItem[] = updatedData.map(item => ({
|
||||
id: item.id,
|
||||
sort_number: item.sort_number,
|
||||
}))
|
||||
|
||||
// 打印调试
|
||||
console.log('更新顺序:', payload)
|
||||
|
||||
// 调用后端接口更新排序
|
||||
updateMoneyCategorySortApi({
|
||||
data: payload,
|
||||
}).then(res => {
|
||||
console.log('后端返回:', res)
|
||||
})
|
||||
|
||||
// 更新前端 state
|
||||
set({ categories: finalCategories })
|
||||
}
|
||||
,
|
||||
|
||||
deleteCategoryById: (id: number, type: number) => {
|
||||
const allCategories = get().categories
|
||||
|
||||
// 删除指定 id,同时只处理同类型的数据
|
||||
const finalCategories = allCategories
|
||||
.filter(c => c.id !== id)
|
||||
.map((item, index) => {
|
||||
if (item.type === type) {
|
||||
return { ...item, sort_number: index }
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
// 更新 state
|
||||
set({ categories: finalCategories })
|
||||
|
||||
// 调用删除接口
|
||||
deleteMoneyCategoryApi({ id })
|
||||
|
||||
// 同步更新排序给后端,仅同类型
|
||||
const payload = finalCategories
|
||||
.filter(c => c.type === type)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
sort_number: item.sort_number,
|
||||
}))
|
||||
updateMoneyCategorySortApi({ data: payload })
|
||||
},
|
||||
|
||||
}),
|
||||
{
|
||||
name: 'money-category-storage',
|
||||
|
||||
@@ -13,3 +13,16 @@ export interface MoneyCategoryItem extends MoneyCategoryBase {
|
||||
}
|
||||
|
||||
export type QueryMoneyCategoryResult = MoneyCategoryBase[]
|
||||
|
||||
export interface DeleteMoneyCategoryParams {
|
||||
id: number
|
||||
}
|
||||
|
||||
export interface UpdateMoneyCategorySortItem {
|
||||
id: number
|
||||
sort_number: number
|
||||
}
|
||||
|
||||
export interface UpdateMoneyCategorySortParams {
|
||||
data: UpdateMoneyCategorySortItem[]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import pandas as pd
|
||||
import random
|
||||
import string
|
||||
|
||||
# 配置
|
||||
rows = 10000
|
||||
cols = 20
|
||||
|
||||
# 生成列名
|
||||
columns = [f"列{i+1}" for i in range(cols)]
|
||||
|
||||
# 生成数据
|
||||
data = []
|
||||
for _ in range(rows):
|
||||
row = []
|
||||
for _ in range(cols):
|
||||
# 随机数字或者随机字符串
|
||||
if random.random() < 0.5:
|
||||
row.append(random.randint(1, 10000)) # 随机数字
|
||||
else:
|
||||
row.append(''.join(random.choices(string.ascii_uppercase, k=5))) # 随机5个字母
|
||||
data.append(row)
|
||||
|
||||
# 创建 DataFrame
|
||||
df = pd.DataFrame(data, columns=columns)
|
||||
|
||||
# 保存 Excel
|
||||
df.to_excel("随机20列10000行.xlsx", index=False)
|
||||
|
||||
# 或者保存 CSV
|
||||
df.to_csv("随机20列10000行.csv", index=False, encoding='utf-8-sig')
|
||||
|
||||
print("生成完成!")
|
||||
@@ -1,337 +1,76 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||
<title>共享空间 — 邀请 / 加入</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg: #f6f7fb;
|
||||
--card: #ffffff;
|
||||
--muted: #9aa4b2;
|
||||
--accent: #5b8cff;
|
||||
--danger: #ff6b6b;
|
||||
--radius: 12px;
|
||||
--gap: 12px;
|
||||
--shadow: 0 6px 18px rgba(22,27,40,0.06);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial;
|
||||
}
|
||||
|
||||
/* Reset / basic */
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
margin:0;
|
||||
background:linear-gradient(180deg, var(--bg), #eef2ff 60%);
|
||||
color:#0f1724;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
-moz-osx-font-smoothing:grayscale;
|
||||
padding:16px;
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.screen{
|
||||
max-width:480px;
|
||||
margin:0 auto;
|
||||
}
|
||||
|
||||
header.appbar{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:12px;
|
||||
padding:8px 4px;
|
||||
margin-bottom:12px;
|
||||
}
|
||||
.title{
|
||||
font-size:18px;
|
||||
font-weight:600;
|
||||
}
|
||||
.subtitle{
|
||||
display:block;
|
||||
font-size:12px;
|
||||
color:var(--muted);
|
||||
margin-top:2px;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card{
|
||||
background:var(--card);
|
||||
border-radius:var(--radius);
|
||||
box-shadow:var(--shadow);
|
||||
padding:14px;
|
||||
margin-bottom:var(--gap);
|
||||
}
|
||||
|
||||
/* Invite area */
|
||||
.invite-top{
|
||||
display:flex;
|
||||
gap:12px;
|
||||
align-items:center;
|
||||
}
|
||||
.space-avatar{
|
||||
width:56px;height:56px;border-radius:12px;background:linear-gradient(135deg,#7fb6ff,#5b8cff);
|
||||
display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:20px;
|
||||
}
|
||||
.space-info{
|
||||
flex:1;
|
||||
}
|
||||
.space-name{font-weight:600;font-size:16px}
|
||||
.space-meta{font-size:12px;color:var(--muted);margin-top:4px}
|
||||
|
||||
.invite-row{
|
||||
margin-top:12px;display:flex;gap:8px;
|
||||
}
|
||||
.btn{
|
||||
flex:1;
|
||||
border:0;padding:10px 12px;border-radius:10px;font-weight:600;
|
||||
background:var(--accent);color:#fff;cursor:pointer;
|
||||
box-shadow:0 6px 12px rgba(91,140,255,0.18);
|
||||
}
|
||||
.btn.ghost{background:transparent;color:var(--accent);border:1px solid rgba(91,140,255,0.12);box-shadow:none}
|
||||
.btn.danger{background:var(--danger);box-shadow:0 6px 12px rgba(255,107,107,0.16)}
|
||||
|
||||
/* Invite code box */
|
||||
.code-box{display:flex;align-items:center;justify-content:space-between;padding:10px;border-radius:10px;background:#f4f7ff;border:1px dashed rgba(91,140,255,0.18)}
|
||||
.code-text{font-weight:700;letter-spacing:2px;font-size:16px}
|
||||
.code-actions{display:flex;gap:8px}
|
||||
|
||||
/* Join section */
|
||||
.join-input{
|
||||
display:flex;gap:8px;margin-top:8px;
|
||||
}
|
||||
.input{
|
||||
flex:1;padding:10px;border-radius:10px;border:1px solid #e6e9ef;background:#fff;
|
||||
font-size:14px;
|
||||
}
|
||||
.help-line{font-size:12px;color:var(--muted);margin-top:8px}
|
||||
|
||||
/* Recent invites / list */
|
||||
.list-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-top:1px solid #f2f4f8}
|
||||
.list-left{display:flex;align-items:center;gap:10px}
|
||||
.small-avatar{width:36px;height:36px;border-radius:8px;background:#eef4ff;display:flex;align-items:center;justify-content:center;color:#3759c8;font-weight:700}
|
||||
.list-title{font-weight:600}
|
||||
.list-sub{font-size:12px;color:var(--muted)}
|
||||
|
||||
/* Modal (simple) */
|
||||
.overlay{position:fixed;left:0;right:0;top:0;bottom:0;background:rgba(8,10,15,0.45);display:flex;align-items:center;justify-content:center;padding:20px;z-index:80}
|
||||
.modal{width:100%;max-width:420px;border-radius:14px;background:var(--card);padding:16px;box-shadow:var(--shadow)}
|
||||
.modal .h1{font-weight:700;margin-bottom:8px}
|
||||
.modal .muted{color:var(--muted);font-size:13px;margin-bottom:12px}
|
||||
.share-grid{display:flex;gap:8px;flex-wrap:wrap}
|
||||
|
||||
.share-chip{flex:0 0 30%;min-width:84px;padding:10px;border-radius:10px;background:#f7f9ff;text-align:center;font-size:13px;color:#123;background:linear-gradient(180deg,#fff,#f7f9ff);cursor:pointer}
|
||||
.copy-toast{position:fixed;left:50%;transform:translateX(-50%);bottom:22px;padding:8px 14px;background:rgba(0,0,0,0.7);color:#fff;border-radius:20px;font-size:13px;z-index:90}
|
||||
|
||||
/* Footer hint */
|
||||
.hint{font-size:12px;color:var(--muted);text-align:center;margin-top:12px}
|
||||
|
||||
/* small screens */
|
||||
@media (max-width:360px){
|
||||
.btn{padding:10px}
|
||||
.space-avatar{width:48px;height:48px;font-size:18px}
|
||||
}
|
||||
</style>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>DataGrid Virtual Scrolling - jQuery EasyUI Demo</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://www.jeasyui.com/easyui/themes/default/easyui.css">
|
||||
<link rel="stylesheet" type="text/css" href="https://www.jeasyui.com/easyui/themes/icon.css">
|
||||
<script type="text/javascript" src="https://www.jeasyui.com/easyui/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="https://www.jeasyui.com/easyui/jquery.easyui.min.js"></script>
|
||||
<script type="text/javascript" src="https://www.jeasyui.com/easyui/datagrid-scrollview.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="screen">
|
||||
<header class="appbar">
|
||||
<div style="width:44px;"></div>
|
||||
<div>
|
||||
<div class="title">共享空间</div>
|
||||
<div class="subtitle">邀请别人加入你的空间,或输入邀请码加入别人空间</div>
|
||||
</div>
|
||||
</header>
|
||||
<h2>DataGrid Virtual Scroll View Demo</h2>
|
||||
<p>Drag the vertical scroll and release it to navigate to the desired page.</p>
|
||||
|
||||
<!-- Invite card -->
|
||||
<section class="card">
|
||||
<div class="invite-top">
|
||||
<div class="space-avatar" id="spaceAvatar">A</div>
|
||||
<div class="space-info">
|
||||
<div class="space-name" id="spaceName">工作协作空间</div>
|
||||
<div class="space-meta">成员 12 • 权限 管理者</div>
|
||||
</div>
|
||||
<div style="width:68px;text-align:right"><button class="btn ghost" id="manageBtn">管理</button></div>
|
||||
<div style="margin-bottom:10px">
|
||||
<select onchange="load(this.value)">
|
||||
<option value="remote">Load Remote Data</option>
|
||||
<option value="local">Load Local Data</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:14px">
|
||||
<div class="code-box" id="codeBox">
|
||||
<div class="code-text" id="inviteCode">AB12-CD34</div>
|
||||
<div class="code-actions">
|
||||
<button class="btn ghost" id="copyBtn">复制</button>
|
||||
<button class="btn" id="shareBtn">分享</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="invite-row">
|
||||
<button class="btn ghost" id="qrBtn">二维码</button>
|
||||
<button class="btn ghost" id="linkBtn">链接</button>
|
||||
<button class="btn danger" id="revokeBtn">撤销并重置</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint">邀请码有效期:24小时 · 点击分享可选择渠道</div>
|
||||
</section>
|
||||
|
||||
<!-- Join other -->
|
||||
<section class="card" aria-label="加入他人空间">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;">
|
||||
<div style="font-weight:700">加入别人的空间</div>
|
||||
<div class="list-sub">通过邀请码或扫码加入</div>
|
||||
</div>
|
||||
|
||||
<div class="join-input">
|
||||
<input class="input" id="joinInput" placeholder="输入邀请码或链接(例如:AB12-CD34)" />
|
||||
<button class="btn" id="joinBtn">加入</button>
|
||||
</div>
|
||||
<div class="help-line">若对方使用链接邀请,可直接粘贴链接;扫码请使用 App 内相机。</div>
|
||||
|
||||
<div style="margin-top:12px">
|
||||
<div style="font-weight:600;margin-bottom:8px">最近加入的空间</div>
|
||||
<div class="list-row">
|
||||
<div class="list-left">
|
||||
<div class="small-avatar">P</div>
|
||||
<div>
|
||||
<div class="list-title">产品设计空间</div>
|
||||
<div class="list-sub">邀请人:小李 · 2 天前</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<div class="list-sub">成员 8</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-row">
|
||||
<div class="list-left">
|
||||
<div class="small-avatar">M</div>
|
||||
<div>
|
||||
<div class="list-title">市场推广</div>
|
||||
<div class="list-sub">邀请人:小王 · 1 周前</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<div class="list-sub">成员 20</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- footer actions -->
|
||||
<div style="display:flex;gap:8px;margin-top:8px">
|
||||
<button class="btn ghost" style="flex:1" id="createSpace">创建新空间</button>
|
||||
<button class="btn" style="flex:1" id="explore">发现空间</button>
|
||||
</div>
|
||||
|
||||
<div style="height:40px"></div>
|
||||
</div>
|
||||
|
||||
<!-- Modal markup (hidden initially) -->
|
||||
<div id="modalOverlay" class="overlay" style="display:none;">
|
||||
<div class="modal" role="dialog" aria-modal="true">
|
||||
<div class="h1">分享邀请</div>
|
||||
<div class="muted">选择一种方式邀请你的成员</div>
|
||||
|
||||
<div class="share-grid" style="margin-bottom:12px;">
|
||||
<div class="share-chip" data-action="wechat">微信</div>
|
||||
<div class="share-chip" data-action="qq">QQ</div>
|
||||
<div class="share-chip" data-action="link">复制链接</div>
|
||||
<div class="share-chip" data-action="sms">短信</div>
|
||||
<div class="share-chip" data-action="email">邮件</div>
|
||||
<div class="share-chip" data-action="more">更多</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||
<button class="btn ghost" onclick="closeModal()">取消</button>
|
||||
<button class="btn" id="modalShare">分享</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- toast -->
|
||||
<div id="copyToast" class="copy-toast" style="display:none;">已复制到剪贴板</div>
|
||||
|
||||
<script>
|
||||
// demo interactions
|
||||
const inviteCodeEl = document.getElementById('inviteCode');
|
||||
const copyBtn = document.getElementById('copyBtn');
|
||||
const shareBtn = document.getElementById('shareBtn');
|
||||
const modal = document.getElementById('modalOverlay');
|
||||
const copyToast = document.getElementById('copyToast');
|
||||
const joinInput = document.getElementById('joinInput');
|
||||
const joinBtn = document.getElementById('joinBtn');
|
||||
|
||||
// copy code
|
||||
copyBtn.addEventListener('click', async () => {
|
||||
const code = inviteCodeEl.textContent.trim();
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
showToast('已复制邀请码');
|
||||
} catch (e) {
|
||||
showToast('复制失败,请手动复制');
|
||||
}
|
||||
});
|
||||
|
||||
// open share modal
|
||||
shareBtn.addEventListener('click', () => openModal());
|
||||
|
||||
document.getElementById('qrBtn').addEventListener('click', ()=> {
|
||||
alert('打开二维码生成器(示例占位)');
|
||||
});
|
||||
document.getElementById('linkBtn').addEventListener('click', ()=> {
|
||||
alert('打开链接分享(示例占位)');
|
||||
});
|
||||
document.getElementById('revokeBtn').addEventListener('click', ()=> {
|
||||
const newCode = randomCode();
|
||||
inviteCodeEl.textContent = newCode;
|
||||
showToast('邀请码已重置');
|
||||
});
|
||||
|
||||
// join
|
||||
joinBtn.addEventListener('click', ()=> {
|
||||
const v = (joinInput.value || '').trim();
|
||||
if (!v) return showToast('请输入邀请码或链接');
|
||||
// 简单验证格式示例
|
||||
const matched = /^[A-Z0-9]{2,}-[A-Z0-9]{2,}$/.test(v) || v.includes('http');
|
||||
if (!matched) return showToast('邀请码格式不正确');
|
||||
showToast('加入请求已发送');
|
||||
// TODO: 调用后端 API 加入空间
|
||||
});
|
||||
|
||||
// modal actions
|
||||
function openModal(){ modal.style.display='flex'; }
|
||||
function closeModal(){ modal.style.display='none'; }
|
||||
|
||||
document.getElementById('modalShare').addEventListener('click', ()=>{
|
||||
// 示例:复制链接并关闭
|
||||
const fakeLink = location.href + '?invite=' + inviteCodeEl.textContent.trim();
|
||||
navigator.clipboard.writeText(fakeLink).catch(()=>{});
|
||||
showToast('邀请链接已复制');
|
||||
closeModal();
|
||||
});
|
||||
|
||||
// copy chips
|
||||
document.querySelectorAll('.share-chip').forEach(el=>{
|
||||
el.addEventListener('click', ()=> {
|
||||
const action = el.getAttribute('data-action');
|
||||
if (action === 'link') {
|
||||
const fakeLink = location.href + '?invite=' + inviteCodeEl.textContent.trim();
|
||||
navigator.clipboard.writeText(fakeLink).then(()=> showToast('链接已复制'));
|
||||
} else {
|
||||
showToast('已选择:' + action);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// small utilities
|
||||
function showToast(text){
|
||||
copyToast.textContent = text;
|
||||
copyToast.style.display = 'block';
|
||||
setTimeout(()=> copyToast.style.display='none', 1500);
|
||||
}
|
||||
|
||||
function randomCode(){
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
const pick = (n)=> Array.from({length:n}).map(()=>chars[Math.floor(Math.random()*chars.length)]).join('');
|
||||
return pick(2)+'-'+pick(4);
|
||||
}
|
||||
</script>
|
||||
<table id="tt" title="DataGrid - VirtualScrollView" style="width:700px;height:300px" data-options="
|
||||
view:scrollview,rownumbers:true,singleSelect:true,
|
||||
url:'datagrid27_getdata.php',
|
||||
idField:'inv',autoRowHeight:false,pageSize:50">
|
||||
<thead>
|
||||
<tr>
|
||||
<th field="inv" width="80">Inv No</th>
|
||||
<th field="date" width="100">Date</th>
|
||||
<th field="name" width="80">Name</th>
|
||||
<th field="amount" width="80" align="right">Amount</th>
|
||||
<th field="price" width="80" align="right">Price</th>
|
||||
<th field="cost" width="100" align="right">Cost</th>
|
||||
<th field="note" width="110">Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
$('#tt').datagrid();
|
||||
});
|
||||
function loadLocal(){
|
||||
var rows = [];
|
||||
for(var i=1; i<=8000; i++){
|
||||
var amount = Math.floor(Math.random()*1000);
|
||||
var price = Math.floor(Math.random()*1000);
|
||||
rows.push({
|
||||
inv: 'Inv No '+i,
|
||||
date: $.fn.datebox.defaults.formatter(new Date()),
|
||||
name: 'Name '+i,
|
||||
amount: amount,
|
||||
price: price,
|
||||
cost: amount*price,
|
||||
note: 'Note '+i
|
||||
});
|
||||
}
|
||||
$('#tt').datagrid('loadData', rows);
|
||||
}
|
||||
function load(mode){
|
||||
if (mode == 'local'){
|
||||
loadLocal();
|
||||
} else {
|
||||
$('#tt').datagrid({
|
||||
url:'datagrid27_getdata.php'
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.datagrid-header-rownumber,.datagrid-cell-rownumber{
|
||||
width:40px;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
@@ -15,12 +15,16 @@ import (
|
||||
)
|
||||
|
||||
func InitMoneyRouter(r *router.Router) {
|
||||
group := r.Group("money")
|
||||
group.POST("/query", queryMoneyRecords)
|
||||
group.GET("/category", getUserCategory)
|
||||
group.POST("/insert", insertMoneyRecord)
|
||||
group.POST("/update", updateMoneyRecord)
|
||||
group.POST("/delete", deleteMoneyRecord)
|
||||
groupRecord := r.Group("money")
|
||||
groupRecord.POST("/query", queryMoneyRecords)
|
||||
groupRecord.POST("/insert", insertMoneyRecord)
|
||||
groupRecord.POST("/update", updateMoneyRecord)
|
||||
groupRecord.POST("/delete", deleteMoneyRecord)
|
||||
|
||||
groupCategory := r.Group("money/category")
|
||||
groupCategory.GET("", getUserCategory)
|
||||
groupCategory.POST("/updateSort", updateMoneyCategorySort)
|
||||
groupCategory.POST("/delete", deleteMoneyCategoryById)
|
||||
}
|
||||
|
||||
// InitDefaultCategories 初始化系统默认分类到用户分类表
|
||||
@@ -300,3 +304,45 @@ func deleteMoneyRecord(c *router.Context) error {
|
||||
|
||||
return response.Success(c).Message("删除成功").Send()
|
||||
}
|
||||
|
||||
func updateMoneyCategorySort(c *router.Context) error {
|
||||
bodyData, err := c.BodyMap()
|
||||
fmt.Println("进入了===")
|
||||
|
||||
if err == nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Println("发生异常===")
|
||||
return c.AddError(err)
|
||||
}
|
||||
|
||||
if bodyData == nil {
|
||||
return response.Success(c).Send()
|
||||
}
|
||||
|
||||
updateArray := bodyData.GetArray("data")
|
||||
|
||||
fmt.Println(updateArray)
|
||||
|
||||
_, err = db.UpdateBatch("money_user_category", "id", updateArray)
|
||||
|
||||
if err != nil {
|
||||
return c.AddError(err)
|
||||
}
|
||||
|
||||
return response.Success(c).Send()
|
||||
}
|
||||
|
||||
func deleteMoneyCategoryById(c *router.Context) error {
|
||||
bodyData, err := c.GetBodyWithRequired("id")
|
||||
if err != nil {
|
||||
return c.AddError(err)
|
||||
}
|
||||
recordId := bodyData.GetInt64("id")
|
||||
|
||||
_, err = db.DeleteOne("money_user_category", "id", recordId)
|
||||
if err != nil {
|
||||
return c.AddError(err)
|
||||
}
|
||||
|
||||
return response.Success(c).Message("删除成功").Send()
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package db
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"base-go-v2/internal/utils/mapx"
|
||||
)
|
||||
|
||||
// ---------------- 内部辅助函数 ----------------
|
||||
@@ -42,10 +44,7 @@ func UpdateOne(table string, idColumn string, id interface{}, data map[string]in
|
||||
|
||||
res, err := tx.Exec(sql, values...)
|
||||
if err != nil {
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
_ = tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -56,8 +55,8 @@ func UpdateOne(table string, idColumn string, id interface{}, data map[string]in
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
// UpdateBatch 批量更新,dataList 中每个 map 必须包含主键 idColumn
|
||||
func UpdateBatch(table string, idColumn string, dataList []map[string]interface{}) (int64, error) {
|
||||
// UpdateBatch 批量更新,dataList 中每个 mapx.M 必须包含主键 idColumn
|
||||
func UpdateBatch(table string, idColumn string, dataList []mapx.M) (int64, error) {
|
||||
if len(dataList) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
@@ -68,30 +67,36 @@ func UpdateBatch(table string, idColumn string, dataList []map[string]interface{
|
||||
}
|
||||
|
||||
total := int64(0)
|
||||
for _, data := range dataList {
|
||||
idValue, ok := data[idColumn]
|
||||
for _, item := range dataList {
|
||||
idValue, ok := item[idColumn]
|
||||
if !ok {
|
||||
tx.Rollback()
|
||||
_ = tx.Rollback()
|
||||
return 0, fmt.Errorf("缺少主键列 %s", idColumn)
|
||||
}
|
||||
|
||||
// 移除主键列,否则会重复在 SET 中出现
|
||||
delete(data, idColumn)
|
||||
// 将 mapx.M 转为 map[string]interface{},并移除主键
|
||||
data := make(map[string]interface{})
|
||||
for k, v := range item {
|
||||
if k != idColumn {
|
||||
data[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
sql, values := buildUpdateSQL(table, data, idColumn)
|
||||
values = append(values, idValue)
|
||||
|
||||
res, err := tx.Exec(sql, values...)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
|
||||
_ = tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
rows, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
_ = tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
total += rows
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,6 @@ func (c *Context) GetBodyWithRequired(keys ...string) (mapx.M, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 校验必填字段
|
||||
if err := validate.NotEmpty(req, keys...); err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in new issue
Block a user