u
This commit is contained in:
1 parent
d51eacb8ad
commit
44b3ef27d2
50 files changed
+3141
-9734
No files matched your search
@@ -1,901 +0,0 @@
|
||||
/**
|
||||
* DataGrid ScrollView for jQuery EasyUI
|
||||
* version: 1.0.2
|
||||
*/
|
||||
|
||||
$.extend($.fn.datagrid.defaults, {
|
||||
rowHeight: null,
|
||||
maxDivHeight: 10000000,
|
||||
maxVisibleHeight: 15000000,
|
||||
deltaTopHeight: 0,
|
||||
onBeforeFetch(page) {},
|
||||
onFetch(page, rows) {},
|
||||
loader(param, success, error) {
|
||||
let opts = $(this).datagrid('options')
|
||||
if (!opts.url)
|
||||
return false
|
||||
if (opts.view.type == 'scrollview') {
|
||||
param.page = param.page || 1
|
||||
param.rows = param.rows || opts.pageSize
|
||||
}
|
||||
$.ajax({
|
||||
type: opts.method,
|
||||
url: opts.url,
|
||||
data: param,
|
||||
dataType: 'json',
|
||||
success(data) {
|
||||
success(data)
|
||||
},
|
||||
error() {
|
||||
error.apply(this, arguments)
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
$.extend($.fn.datagrid.defaults.finder, {
|
||||
getRow(target, p) { // p can be row index or tr object
|
||||
let index = (typeof p == 'object') ? p.attr('datagrid-row-index') : p
|
||||
let data = $.data(target, 'datagrid').data
|
||||
let opts = $(target).datagrid('options')
|
||||
if (opts.view.type == 'scrollview') {
|
||||
let firstRows = data.firstRows || []
|
||||
if (index < firstRows.length) {
|
||||
return firstRows[index]
|
||||
}
|
||||
else {
|
||||
index -= opts.view.index
|
||||
}
|
||||
}
|
||||
return data.rows[index]
|
||||
},
|
||||
})
|
||||
|
||||
let scrollview = $.extend({}, $.fn.datagrid.defaults.view, {
|
||||
type: 'scrollview',
|
||||
index: 0,
|
||||
r1: [],
|
||||
r2: [],
|
||||
rows: [],
|
||||
render(target, container, frozen) {
|
||||
let state = $.data(target, 'datagrid')
|
||||
let opts = state.options
|
||||
let rows = this.rows || []
|
||||
if (!rows.length) {
|
||||
return
|
||||
}
|
||||
let fields = $(target).datagrid('getColumnFields', frozen)
|
||||
|
||||
if (frozen) {
|
||||
if (!(opts.rownumbers || (opts.frozenColumns && opts.frozenColumns.length))) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let index = this.index
|
||||
let table = ['<div class="datagrid-btable-top"></div>', '<table class="datagrid-btable" cellspacing="0" cellpadding="0" border="0"><tbody>']
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let css = opts.rowStyler ? opts.rowStyler.call(target, index, rows[i]) : ''
|
||||
let classValue = ''
|
||||
let styleValue = ''
|
||||
if (typeof css == 'string') {
|
||||
styleValue = css
|
||||
}
|
||||
else if (css) {
|
||||
classValue = css.class || ''
|
||||
styleValue = css.style || ''
|
||||
}
|
||||
let cls = `class="datagrid-row ${index % 2 && opts.striped ? 'datagrid-row-alt ' : ' '}${classValue}"`
|
||||
let style = styleValue ? `style="${styleValue}"` : ''
|
||||
let rowId = `${state.rowIdPrefix}-${frozen ? 1 : 2}-${index}`
|
||||
table.push(`<tr id="${rowId}" datagrid-row-index="${index}" ${cls} ${style}>`)
|
||||
table.push(this.renderRow.call(this, target, fields, frozen, index, rows[i]))
|
||||
table.push('</tr>')
|
||||
|
||||
// render the detail row
|
||||
if (opts.detailFormatter) {
|
||||
table.push('<tr style="display:none;">')
|
||||
if (frozen) {
|
||||
table.push(`<td colspan=${fields.length + (opts.rownumbers ? 1 : 0)} style="border-right:0">`)
|
||||
}
|
||||
else {
|
||||
table.push(`<td colspan=${fields.length}>`)
|
||||
}
|
||||
table.push('<div class="datagrid-row-detail">')
|
||||
if (frozen) {
|
||||
table.push(' ')
|
||||
}
|
||||
else {
|
||||
table.push(opts.detailFormatter.call(target, index, rows[i]))
|
||||
}
|
||||
table.push('</div>')
|
||||
table.push('</td>')
|
||||
table.push('</tr>')
|
||||
}
|
||||
|
||||
index++
|
||||
}
|
||||
table.push('</tbody></table>')
|
||||
table.push('<div class="datagrid-btable-bottom"></div>')
|
||||
|
||||
$(container).html(table.join(''))
|
||||
},
|
||||
|
||||
renderRow(target, fields, frozen, rowIndex, rowData) {
|
||||
let opts = $.data(target, 'datagrid').options
|
||||
|
||||
let cc = []
|
||||
if (frozen && opts.rownumbers) {
|
||||
let rownumber = rowIndex + 1
|
||||
// if (opts.pagination){
|
||||
// rownumber += (opts.pageNumber-1)*opts.pageSize;
|
||||
// }
|
||||
cc.push(`<td class="datagrid-td-rownumber"><div class="datagrid-cell-rownumber">${rownumber}</div></td>`)
|
||||
}
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
let field = fields[i]
|
||||
let col = $(target).datagrid('getColumnOption', field)
|
||||
if (col) {
|
||||
let value = rowData[field] // the field value
|
||||
let css = col.styler ? (col.styler(value, rowData, rowIndex) || '') : ''
|
||||
let classValue = ''
|
||||
let styleValue = ''
|
||||
if (typeof css == 'string') {
|
||||
styleValue = css
|
||||
}
|
||||
else if (cc) {
|
||||
classValue = css.class || ''
|
||||
styleValue = css.style || ''
|
||||
}
|
||||
let cls = classValue ? `class="${classValue}"` : ''
|
||||
let style = col.hidden ? `style="display:none;${styleValue}"` : (styleValue ? `style="${styleValue}"` : '')
|
||||
|
||||
cc.push(`<td field="${field}" ${cls} ${style}>`)
|
||||
|
||||
if (col.checkbox) {
|
||||
style = ''
|
||||
}
|
||||
else if (col.expander) {
|
||||
style = 'text-align:center;height:16px;'
|
||||
}
|
||||
else {
|
||||
style = styleValue
|
||||
if (col.align) { style += `;text-align:${col.align};` }
|
||||
if (!opts.nowrap) {
|
||||
style += ';white-space:normal;height:auto;'
|
||||
}
|
||||
else if (opts.autoRowHeight) {
|
||||
style += ';height:auto;'
|
||||
}
|
||||
}
|
||||
|
||||
cc.push(`<div style="${style}" `)
|
||||
if (col.checkbox) {
|
||||
cc.push('class="datagrid-cell-check ')
|
||||
}
|
||||
else {
|
||||
cc.push(`class="datagrid-cell ${col.cellClass}`)
|
||||
}
|
||||
cc.push('">')
|
||||
|
||||
if (col.checkbox) {
|
||||
cc.push(`<input type="checkbox" name="${field}" value="${value != undefined ? value : ''}">`)
|
||||
}
|
||||
else if (col.expander) {
|
||||
// cc.push('<div style="text-align:center;width:16px;height:16px;">');
|
||||
cc.push('<span class="datagrid-row-expander datagrid-row-expand" style="display:inline-block;width:16px;height:16px;cursor:pointer;" />')
|
||||
// cc.push('</div>');
|
||||
}
|
||||
else if (col.formatter) {
|
||||
cc.push(col.formatter(value, rowData, rowIndex))
|
||||
}
|
||||
else {
|
||||
cc.push(value)
|
||||
}
|
||||
|
||||
cc.push('</div>')
|
||||
cc.push('</td>')
|
||||
}
|
||||
}
|
||||
return cc.join('')
|
||||
},
|
||||
|
||||
bindEvents(target) {
|
||||
let state = $.data(target, 'datagrid')
|
||||
let dc = state.dc
|
||||
let opts = state.options
|
||||
let body = dc.body1.add(dc.body2)
|
||||
let clickHandler = ($.data(body[0], 'events') || $._data(body[0], 'events')).click[0].handler
|
||||
body.unbind('click').bind('click', (e) => {
|
||||
let tt = $(e.target)
|
||||
let tr = tt.closest('tr.datagrid-row')
|
||||
if (!tr.length) { return }
|
||||
if (tt.hasClass('datagrid-row-expander')) {
|
||||
let rowIndex = Number.parseInt(tr.attr('datagrid-row-index'))
|
||||
if (tt.hasClass('datagrid-row-expand')) {
|
||||
$(target).datagrid('expandRow', rowIndex)
|
||||
}
|
||||
else {
|
||||
$(target).datagrid('collapseRow', rowIndex)
|
||||
}
|
||||
$(target).datagrid('fixRowHeight')
|
||||
}
|
||||
else {
|
||||
clickHandler(e)
|
||||
}
|
||||
e.stopPropagation()
|
||||
})
|
||||
},
|
||||
|
||||
onBeforeRender(target) {
|
||||
let state = $.data(target, 'datagrid')
|
||||
let opts = state.options
|
||||
let dc = state.dc
|
||||
let view = this
|
||||
|
||||
opts._emptyMsg = opts.emptyMsg // store the emptyMsg value
|
||||
opts.emptyMsg = '' // erase it to prevent from displaying it
|
||||
state.data.firstRows = state.data.rows
|
||||
state.data.rows = []
|
||||
|
||||
dc.body1.add(dc.body2).empty()
|
||||
this.rows = [] // the rows to be rendered
|
||||
this.r1 = this.r2 = [] // the first part and last part of rows
|
||||
|
||||
init()
|
||||
createHeaderExpander()
|
||||
|
||||
function init() {
|
||||
opts.rowHeight = $(target).datagrid('getRowHeight')
|
||||
let pager = $(target).datagrid('getPager')
|
||||
pager.each(function () {
|
||||
$(this).pagination('options').onSelectPage = function (pageNum, pageSize) {
|
||||
opts.pageNumber = pageNum || 1
|
||||
opts.pageSize = pageSize
|
||||
pager.pagination('refresh', {
|
||||
pageNumber: pageNum,
|
||||
pageSize,
|
||||
})
|
||||
$(target).datagrid('gotoPage', opts.pageNumber)
|
||||
}
|
||||
})
|
||||
// erase the onLoadSuccess event, make sure it can't be triggered
|
||||
state.onLoadSuccess = opts.onLoadSuccess
|
||||
opts.onLoadSuccess = function () {}
|
||||
if (!opts.remoteSort) {
|
||||
let onBeforeSortColumn = opts.onBeforeSortColumn
|
||||
opts.onBeforeSortColumn = function (name, order) {
|
||||
let result = onBeforeSortColumn.call(this, name, order)
|
||||
if (result == false) {
|
||||
return false
|
||||
}
|
||||
state.data.rows = state.data.firstRows
|
||||
}
|
||||
}
|
||||
dc.body2.unbind('.datagrid')
|
||||
setTimeout(() => {
|
||||
dc.body2.unbind('.datagrid').bind('scroll.datagrid', (e) => {
|
||||
if (state.onLoadSuccess) {
|
||||
opts.onLoadSuccess = state.onLoadSuccess // restore the onLoadSuccess event
|
||||
state.onLoadSuccess = undefined
|
||||
state.originalRows = $.extend(true, [], state.data.firstRows)
|
||||
}
|
||||
if (view.scrollTimer) {
|
||||
clearTimeout(view.scrollTimer)
|
||||
}
|
||||
view.scrollTimer = setTimeout(() => {
|
||||
view.scrolling.call(view, target)
|
||||
}, 50)
|
||||
})
|
||||
dc.body2.triggerHandler('scroll.datagrid')
|
||||
}, 0)
|
||||
}
|
||||
function createHeaderExpander() {
|
||||
if (!opts.detailFormatter) { return }
|
||||
|
||||
var t = $(target)
|
||||
let hasExpander = false
|
||||
let fields = t.datagrid('getColumnFields', true).concat(t.datagrid('getColumnFields'))
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
let col = t.datagrid('getColumnOption', fields[i])
|
||||
if (col.expander) {
|
||||
hasExpander = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!hasExpander) {
|
||||
if (opts.frozenColumns && opts.frozenColumns.length) {
|
||||
opts.frozenColumns[0].splice(0, 0, { field: '_expander', expander: true, width: 24, resizable: false, fixed: true })
|
||||
}
|
||||
else {
|
||||
opts.frozenColumns = [[{ field: '_expander', expander: true, width: 24, resizable: false, fixed: true }]]
|
||||
}
|
||||
|
||||
var t = dc.view1.children('div.datagrid-header').find('table')
|
||||
let td = $(`<td rowspan="${opts.frozenColumns.length}"><div class="datagrid-header-expander" style="width:24px;"></div></td>`)
|
||||
if ($('tr', t).length == 0) {
|
||||
td.wrap('<tr></tr>').parent().appendTo($('tbody', t))
|
||||
}
|
||||
else if (opts.rownumbers) {
|
||||
td.insertAfter(t.find('td:has(div.datagrid-header-rownumber)'))
|
||||
}
|
||||
else {
|
||||
td.prependTo(t.find('tr:first'))
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
view.bindEvents(target)
|
||||
}, 0)
|
||||
}
|
||||
},
|
||||
|
||||
onAfterRender(target) {
|
||||
$.fn.datagrid.defaults.view.onAfterRender.call(this, target)
|
||||
let dc = $.data(target, 'datagrid').dc
|
||||
let footer = dc.footer1.add(dc.footer2)
|
||||
footer.find('span.datagrid-row-expander').css('visibility', 'hidden')
|
||||
},
|
||||
|
||||
scrolling(target) {
|
||||
let state = $.data(target, 'datagrid')
|
||||
let opts = state.options
|
||||
let dc = state.dc
|
||||
if (!opts.finder.getRows(target).length) {
|
||||
this.reload.call(this, target)
|
||||
}
|
||||
else {
|
||||
if (!dc.body2.is(':visible')) { return }
|
||||
let headerHeight = dc.view2.children('div.datagrid-header').outerHeight()
|
||||
|
||||
let topDiv = dc.body2.children('div.datagrid-btable-top')
|
||||
let bottomDiv = dc.body2.children('div.datagrid-btable-bottom')
|
||||
if (!topDiv.length || !bottomDiv.length) { return }
|
||||
let top = topDiv.position().top + topDiv._outerHeight() - headerHeight
|
||||
let bottom = bottomDiv.position().top - headerHeight
|
||||
top = Math.floor(top)
|
||||
bottom = Math.floor(bottom)
|
||||
|
||||
if (top > dc.body2.height() || bottom < 0) {
|
||||
this.reload.call(this, target)
|
||||
}
|
||||
else if (top > 0) {
|
||||
var page = Math.floor(this.index / opts.pageSize)
|
||||
this.getRows.call(this, target, page, function (rows) {
|
||||
this.page = page
|
||||
this.r2 = this.r1
|
||||
this.r1 = rows
|
||||
this.index = (page - 1) * opts.pageSize
|
||||
this.rows = this.r1.concat(this.r2)
|
||||
this.populate.call(this, target)
|
||||
})
|
||||
}
|
||||
else if (bottom < dc.body2.height()) {
|
||||
if (state.data.rows.length + this.index >= state.data.total) {
|
||||
return
|
||||
}
|
||||
var page = Math.floor(this.index / opts.pageSize) + 2
|
||||
if (this.r2.length) {
|
||||
page++
|
||||
}
|
||||
this.getRows.call(this, target, page, function (rows) {
|
||||
this.page = page
|
||||
if (!this.r2.length) {
|
||||
this.r2 = rows
|
||||
}
|
||||
else {
|
||||
this.r1 = this.r2
|
||||
this.r2 = rows
|
||||
this.index += opts.pageSize
|
||||
}
|
||||
this.rows = this.r1.concat(this.r2)
|
||||
this.populate.call(this, target)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
reload(target) {
|
||||
let state = $.data(target, 'datagrid')
|
||||
let opts = state.options
|
||||
let dc = state.dc
|
||||
let top = $(dc.body2).scrollTop() + opts.deltaTopHeight
|
||||
let index = Math.floor(top / opts.rowHeight)
|
||||
let page = Math.floor(index / opts.pageSize) + 1
|
||||
|
||||
this.getRows.call(this, target, page, function (rows) {
|
||||
this.page = page
|
||||
this.index = (page - 1) * opts.pageSize
|
||||
this.rows = rows
|
||||
this.r1 = rows
|
||||
this.r2 = []
|
||||
this.populate.call(this, target)
|
||||
dc.body2.triggerHandler('scroll.datagrid')
|
||||
})
|
||||
},
|
||||
|
||||
getRows(target, page, callback) {
|
||||
let state = $.data(target, 'datagrid')
|
||||
let opts = state.options
|
||||
let index = (page - 1) * opts.pageSize
|
||||
|
||||
// if possible display the empty message
|
||||
opts.emptyMsg = opts._emptyMsg
|
||||
if (this.setEmptyMsg) {
|
||||
this.setEmptyMsg(target)
|
||||
}
|
||||
|
||||
if (index < 0) { return }
|
||||
if (opts.onBeforeFetch.call(target, page) == false) { return }
|
||||
|
||||
let rows = (state.data.firstRows || []).slice(index, index + opts.pageSize)
|
||||
if (rows.length && (rows.length == opts.pageSize || index + rows.length == state.data.total)) {
|
||||
opts.onFetch.call(target, page, rows)
|
||||
callback.call(this, rows)
|
||||
}
|
||||
else {
|
||||
let param = $.extend({}, opts.queryParams, {
|
||||
page,
|
||||
rows: opts.pageSize,
|
||||
})
|
||||
if (opts.sortName) {
|
||||
$.extend(param, {
|
||||
sort: opts.sortName,
|
||||
order: opts.sortOrder,
|
||||
})
|
||||
}
|
||||
if (opts.onBeforeLoad.call(target, param) == false)
|
||||
return
|
||||
|
||||
$(target).datagrid('loading')
|
||||
let result = opts.loader.call(target, param, (data) => {
|
||||
$(target).datagrid('loaded')
|
||||
var data = opts.loadFilter.call(target, data)
|
||||
opts.onFetch.call(target, page, data.rows)
|
||||
if (data.rows && data.rows.length) {
|
||||
callback.call(opts.view, data.rows)
|
||||
}
|
||||
else {
|
||||
opts.onLoadSuccess.call(target, data)
|
||||
}
|
||||
}, function () {
|
||||
$(target).datagrid('loaded')
|
||||
opts.onLoadError.apply(target, arguments)
|
||||
})
|
||||
if (result == false) {
|
||||
$(target).datagrid('loaded')
|
||||
if (!(state.data.firstRows || []).length) {
|
||||
opts.onFetch.call(target, page, state.data.firstRows)
|
||||
opts.onLoadSuccess.call(target, state.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
populate(target) {
|
||||
let state = $.data(target, 'datagrid')
|
||||
let opts = state.options
|
||||
let dc = state.dc
|
||||
let rowHeight = opts.rowHeight
|
||||
let maxHeight = opts.maxDivHeight
|
||||
|
||||
if (this.rows.length) {
|
||||
opts.view.render.call(opts.view, target, dc.body2, false)
|
||||
opts.view.render.call(opts.view, target, dc.body1, true)
|
||||
|
||||
let body = dc.body1.add(dc.body2)
|
||||
let topDiv = body.children('div.datagrid-btable-top')
|
||||
let bottomDiv = body.children('div.datagrid-btable-bottom')
|
||||
let topHeight = this.index * rowHeight
|
||||
let bottomHeight = state.data.total * rowHeight - this.rows.length * rowHeight - topHeight
|
||||
fillHeight(topDiv, topHeight)
|
||||
fillHeight(bottomDiv, bottomHeight)
|
||||
|
||||
state.data.rows = this.rows
|
||||
|
||||
let spos = dc.body2.scrollTop() + opts.deltaTopHeight
|
||||
if (topHeight > opts.maxVisibleHeight) {
|
||||
opts.deltaTopHeight = topHeight - opts.maxVisibleHeight
|
||||
fillHeight(topDiv, topHeight - opts.deltaTopHeight)
|
||||
}
|
||||
else {
|
||||
opts.deltaTopHeight = 0
|
||||
}
|
||||
if (bottomHeight > opts.maxVisibleHeight) {
|
||||
fillHeight(bottomDiv, opts.maxVisibleHeight)
|
||||
}
|
||||
else if (bottomHeight == 0) {
|
||||
let lastCount = state.data.total % opts.pageSize
|
||||
if (lastCount) {
|
||||
fillHeight(bottomDiv, dc.body2.height() - lastCount * rowHeight)
|
||||
}
|
||||
}
|
||||
|
||||
$(target).datagrid('setSelectionState')
|
||||
dc.body2.scrollTop(spos - opts.deltaTopHeight)
|
||||
|
||||
opts.pageNumber = this.page
|
||||
let pager = $(target).datagrid('getPager')
|
||||
if (pager.length) {
|
||||
let popts = pager.pagination('options')
|
||||
let displayMsg = popts.displayMsg
|
||||
let msg = displayMsg.replace(/\{from\}/, this.index + 1)
|
||||
msg = msg.replace(/\{to\}/, this.index + this.rows.length)
|
||||
pager.pagination('refresh', {
|
||||
pageNumber: this.page,
|
||||
displayMsg: msg,
|
||||
})
|
||||
popts.displayMsg = displayMsg
|
||||
}
|
||||
if (this.setEmptyMsg) {
|
||||
this.setEmptyMsg(target)
|
||||
}
|
||||
|
||||
opts.onLoadSuccess.call(target, {
|
||||
total: state.data.total,
|
||||
rows: this.rows,
|
||||
})
|
||||
}
|
||||
function fillHeight(div, height) {
|
||||
let count = Math.floor(height / maxHeight)
|
||||
let leftHeight = height - maxHeight * count
|
||||
if (height < 0) {
|
||||
leftHeight = 0
|
||||
}
|
||||
let cc = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
cc.push(`<div style="height:${maxHeight}px"></div>`)
|
||||
}
|
||||
cc.push(`<div style="height:${leftHeight}px"></div>`)
|
||||
$(div).html(cc.join(''))
|
||||
}
|
||||
},
|
||||
|
||||
updateRow(target, rowIndex, row) {
|
||||
let opts = $.data(target, 'datagrid').options
|
||||
let rows = $(target).datagrid('getRows')
|
||||
let rowData = opts.finder.getRow(target, rowIndex)
|
||||
let oldStyle = _getRowStyle(rowIndex)
|
||||
$.extend(rowData, row)
|
||||
let newStyle = _getRowStyle(rowIndex)
|
||||
let oldClassValue = oldStyle.c
|
||||
let styleValue = newStyle.s
|
||||
let classValue = `datagrid-row ${rowIndex % 2 && opts.striped ? 'datagrid-row-alt ' : ' '}${newStyle.c}`
|
||||
|
||||
function _getRowStyle(rowIndex) {
|
||||
let css = opts.rowStyler ? opts.rowStyler.call(target, rowIndex, rowData) : ''
|
||||
let classValue = ''
|
||||
let styleValue = ''
|
||||
if (typeof css == 'string') {
|
||||
styleValue = css
|
||||
}
|
||||
else if (css) {
|
||||
classValue = css.class || ''
|
||||
styleValue = css.style || ''
|
||||
}
|
||||
return { c: classValue, s: styleValue }
|
||||
}
|
||||
function _update(frozen) {
|
||||
let fields = $(target).datagrid('getColumnFields', frozen)
|
||||
let tr = opts.finder.getTr(target, rowIndex, 'body', (frozen ? 1 : 2))
|
||||
let checked = tr.find('div.datagrid-cell-check input[type=checkbox]').is(':checked')
|
||||
tr.html(this.renderRow.call(this, target, fields, frozen, rowIndex, rowData))
|
||||
tr.attr('style', styleValue).removeClass(oldClassValue).addClass(classValue)
|
||||
if (checked) {
|
||||
tr.find('div.datagrid-cell-check input[type=checkbox]')._propAttr('checked', true)
|
||||
}
|
||||
}
|
||||
|
||||
_update.call(this, true)
|
||||
_update.call(this, false)
|
||||
$(target).datagrid('fixRowHeight', rowIndex)
|
||||
},
|
||||
|
||||
// insertRow: function(target, index, row){
|
||||
// var state = $.data(target, 'datagrid');
|
||||
// var data = state.data;
|
||||
|
||||
// if (index == undefined || index == null) index = data.rows.length;
|
||||
// if (index > data.rows.length) index = data.rows.length;
|
||||
// $.fn.datagrid.defaults.view.insertRow.call(this, target, index, row);
|
||||
// if (data.firstRows && index <= data.firstRows.length){
|
||||
// data.firstRows.splice(index, 0, row);
|
||||
// }
|
||||
// },
|
||||
insertRow(target, index, row) {
|
||||
let state = $.data(target, 'datagrid')
|
||||
let opts = state.options
|
||||
let data = state.data
|
||||
|
||||
let total = $(target).datagrid('getData').total
|
||||
if (index == null) { index = total }
|
||||
if (index > total) { index = total }
|
||||
if (data.firstRows && index <= data.firstRows.length) {
|
||||
data.firstRows.splice(index, 0, row)
|
||||
}
|
||||
data.total++
|
||||
|
||||
let rows = this.r1.concat(this.r2)
|
||||
if (index < this.index) {
|
||||
this.reload.call(this, target)
|
||||
}
|
||||
else if (index <= this.index + rows.length) {
|
||||
rows.splice(index - this.index, 0, row)
|
||||
this.r1 = rows.splice(0, opts.pageSize)
|
||||
if (this.r2.length) {
|
||||
this.r2 = rows.splice(0, opts.pageSize)
|
||||
}
|
||||
this.rows = this.r1.concat(this.r2)
|
||||
this.populate.call(this, target)
|
||||
state.dc.body2.triggerHandler('scroll.datagrid')
|
||||
}
|
||||
},
|
||||
|
||||
// deleteRow: function(target, index){
|
||||
// var data = $(target).datagrid('getData');
|
||||
// $.fn.datagrid.defaults.view.deleteRow.call(this, target, index);
|
||||
// if (data.firstRows){
|
||||
// data.firstRows.splice(index, 1);
|
||||
// }
|
||||
// },
|
||||
deleteRow(target, index) {
|
||||
let state = $.data(target, 'datagrid')
|
||||
let data = state.data
|
||||
let opts = state.options
|
||||
data.total--
|
||||
if (data.firstRows) {
|
||||
if (index < data.firstRows.length) {
|
||||
data.firstRows.splice(index, 1)
|
||||
if (data.total) {
|
||||
this.reload.call(this, target)
|
||||
}
|
||||
else {
|
||||
// $(target).datagrid('loadData', [])
|
||||
state.data.rows = []
|
||||
$(state.dc.body1).empty()
|
||||
$(state.dc.body2).empty()
|
||||
if (this.setEmptyMsg) {
|
||||
this.setEmptyMsg.call(this, target)
|
||||
}
|
||||
}
|
||||
// this.reload.call(this, target);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let rows = this.r1.concat(this.r2)
|
||||
if (index < this.index) {
|
||||
this.reload.call(this, target)
|
||||
}
|
||||
else if (index < this.index + rows.length) {
|
||||
rows.splice(index - this.index, 1)
|
||||
this.r1 = rows.splice(0, opts.pageSize)
|
||||
if (this.r1.length < opts.pageSize) {
|
||||
this.reload.call(this, target)
|
||||
}
|
||||
else {
|
||||
this.r2 = []
|
||||
this.rows = this.r1.concat(this.r2)
|
||||
this.populate.call(this, target)
|
||||
state.dc.body2.triggerHandler('scroll.datagrid')
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
$.fn.datagrid.methods.baseUpdateRow = $.fn.datagrid.methods.updateRow
|
||||
$.fn.datagrid.methods.baseGetRowIndex = $.fn.datagrid.methods.getRowIndex
|
||||
$.fn.datagrid.methods.baseScrollTo = $.fn.datagrid.methods.scrollTo
|
||||
$.fn.datagrid.methods.baseGotoPage = $.fn.datagrid.methods.gotoPage
|
||||
$.fn.datagrid.methods.baseSetSelectionState = $.fn.datagrid.methods.setSelectionState
|
||||
$.extend($.fn.datagrid.methods, {
|
||||
getRowHeight(jq) {
|
||||
let opts = jq.datagrid('options')
|
||||
if (!opts.rowHeight) {
|
||||
let d = $('<div style="position:absolute;top:-1000px;width:100px;height:100px;padding:5px"><table><tr class="datagrid-row"><td>cell</td></tr></table></div>').appendTo('body')
|
||||
let rowHeight = d.find('tr').outerHeight()
|
||||
d.remove()
|
||||
opts.rowHeight = rowHeight
|
||||
}
|
||||
return opts.rowHeight
|
||||
},
|
||||
updateRow(jq, param) {
|
||||
return jq.each(function () {
|
||||
let opts = $(this).datagrid('options')
|
||||
let row = opts.finder.getRow(this, param.index)
|
||||
if (row) {
|
||||
$(this).datagrid('baseUpdateRow', param)
|
||||
}
|
||||
else {
|
||||
let firstRows = $(this).datagrid('getData').firstRows || []
|
||||
if (param.index < firstRows.length) {
|
||||
$.extend(firstRows[param.index], param.row)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
getRowIndex(jq, row) {
|
||||
let opts = jq.datagrid('options')
|
||||
if (opts.view.type == 'scrollview') {
|
||||
let data = jq.datagrid('getData')
|
||||
let firstRows = data.firstRows || []
|
||||
if (typeof row == 'object') {
|
||||
var index = $.easyui.indexOfArray(firstRows, row)
|
||||
}
|
||||
else {
|
||||
var index = $.easyui.indexOfArray(firstRows, opts.idField, row)
|
||||
}
|
||||
if (index >= 0) {
|
||||
return index
|
||||
}
|
||||
else {
|
||||
index = jq.datagrid('baseGetRowIndex', row)
|
||||
return (index == -1) ? -1 : index + opts.view.index
|
||||
}
|
||||
}
|
||||
else {
|
||||
return jq.datagrid('baseGetRowIndex', row)
|
||||
}
|
||||
},
|
||||
getRow(jq, index) {
|
||||
return jq.datagrid('options').finder.getRow(jq[0], index)
|
||||
},
|
||||
gotoPage(jq, param) {
|
||||
return jq.each(function () {
|
||||
let target = this
|
||||
let opts = $(target).datagrid('options')
|
||||
if (opts.view.type == 'scrollview') {
|
||||
let page, callback
|
||||
if (typeof param == 'object') {
|
||||
page = param.page
|
||||
callback = param.callback
|
||||
}
|
||||
else {
|
||||
page = param
|
||||
}
|
||||
opts.view.getRows.call(opts.view, target, page, function (rows) {
|
||||
this.page = page
|
||||
this.index = (page - 1) * opts.pageSize
|
||||
this.rows = rows
|
||||
this.r1 = rows
|
||||
this.r2 = []
|
||||
this.populate.call(this, target)
|
||||
$(target).data('datagrid').dc.body2.scrollTop(this.index * opts.rowHeight - opts.deltaTopHeight)
|
||||
if (callback) {
|
||||
callback.call(target, page)
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
$(target).datagrid('baseGotoPage', param)
|
||||
}
|
||||
})
|
||||
},
|
||||
scrollTo(jq, param) {
|
||||
return jq.each(function () {
|
||||
let target = this
|
||||
let opts = $(target).datagrid('options')
|
||||
let index, callback
|
||||
if (typeof param == 'object') {
|
||||
index = param.index
|
||||
callback = param.callback
|
||||
}
|
||||
else {
|
||||
index = param
|
||||
}
|
||||
let view = opts.view
|
||||
if (view.type == 'scrollview') {
|
||||
if (index >= view.index && index < view.index + view.rows.length) {
|
||||
$(target).datagrid('baseScrollTo', index)
|
||||
if (callback) {
|
||||
callback.call(target, index)
|
||||
}
|
||||
}
|
||||
else if (index >= 0) {
|
||||
let page = Math.floor(index / opts.pageSize) + 1
|
||||
$(target).datagrid('gotoPage', {
|
||||
page,
|
||||
callback() {
|
||||
setTimeout(() => {
|
||||
$(target).datagrid('baseScrollTo', index)
|
||||
if (callback) {
|
||||
callback.call(target, index)
|
||||
}
|
||||
}, 0)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
$(target).datagrid('baseScrollTo', index)
|
||||
if (callback) {
|
||||
callback.call(target, index)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
setSelectionState(jq) {
|
||||
return jq.each(function () {
|
||||
let target = this
|
||||
let opts = $(target).datagrid('options')
|
||||
if (opts.view.type == 'scrollview') {
|
||||
$(target).datagrid('baseSetSelectionState')
|
||||
let state = $(target).data('datagrid')
|
||||
if ((state.data.firstRows || []).length != state.checkedRows.length) {
|
||||
let dc = state.dc
|
||||
dc.header1.add(dc.header2).find('input[type=checkbox]')._propAttr('checked', false)
|
||||
}
|
||||
}
|
||||
else {
|
||||
$(target).datagrid('baseSetSelectionState')
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
$.extend($.fn.datagrid.methods, {
|
||||
fixDetailRowHeight(jq, index) {
|
||||
return jq.each(function () {
|
||||
let opts = $.data(this, 'datagrid').options
|
||||
let dc = $.data(this, 'datagrid').dc
|
||||
let tr1 = opts.finder.getTr(this, index, 'body', 1).next()
|
||||
let tr2 = opts.finder.getTr(this, index, 'body', 2).next()
|
||||
// fix the detail row height
|
||||
if (tr2.is(':visible')) {
|
||||
tr1.css('height', '')
|
||||
tr2.css('height', '')
|
||||
let height = Math.max(tr1.height(), tr2.height())
|
||||
tr1.css('height', height)
|
||||
tr2.css('height', height)
|
||||
}
|
||||
dc.body2.triggerHandler('scroll')
|
||||
})
|
||||
},
|
||||
getExpander(jq, index) { // get row expander object
|
||||
let opts = $.data(jq[0], 'datagrid').options
|
||||
return opts.finder.getTr(jq[0], index).find('span.datagrid-row-expander')
|
||||
},
|
||||
// get row detail container
|
||||
getRowDetail(jq, index) {
|
||||
let opts = $.data(jq[0], 'datagrid').options
|
||||
let tr = opts.finder.getTr(jq[0], index, 'body', 2)
|
||||
return tr.next().find('div.datagrid-row-detail')
|
||||
},
|
||||
expandRow(jq, index) {
|
||||
return jq.each(function () {
|
||||
let opts = $(this).datagrid('options')
|
||||
let dc = $.data(this, 'datagrid').dc
|
||||
let expander = $(this).datagrid('getExpander', index)
|
||||
if (expander.hasClass('datagrid-row-expand')) {
|
||||
expander.removeClass('datagrid-row-expand').addClass('datagrid-row-collapse')
|
||||
let tr1 = opts.finder.getTr(this, index, 'body', 1).next()
|
||||
let tr2 = opts.finder.getTr(this, index, 'body', 2).next()
|
||||
tr1.show()
|
||||
tr2.show()
|
||||
$(this).datagrid('fixDetailRowHeight', index)
|
||||
if (opts.onExpandRow) {
|
||||
// var row = $(this).datagrid('getRows')[index];
|
||||
let row = $(this).datagrid('getRow', index)
|
||||
opts.onExpandRow.call(this, index, row)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
collapseRow(jq, index) {
|
||||
return jq.each(function () {
|
||||
let opts = $(this).datagrid('options')
|
||||
let dc = $.data(this, 'datagrid').dc
|
||||
let expander = $(this).datagrid('getExpander', index)
|
||||
if (expander.hasClass('datagrid-row-collapse')) {
|
||||
expander.removeClass('datagrid-row-collapse').addClass('datagrid-row-expand')
|
||||
let tr1 = opts.finder.getTr(this, index, 'body', 1).next()
|
||||
let tr2 = opts.finder.getTr(this, index, 'body', 2).next()
|
||||
tr1.hide()
|
||||
tr2.hide()
|
||||
dc.body2.triggerHandler('scroll')
|
||||
if (opts.onCollapseRow) {
|
||||
let row = $(this).datagrid('getRows')[index]
|
||||
opts.onCollapseRow.call(this, index, row)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,19 @@
|
||||
import { Text, View } from 'react-native'
|
||||
import { ACell, ACellGroup } from '@/components/common/ao'
|
||||
import { AButton, ACell, ACellGroup } from '@/components/common/ao'
|
||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||
|
||||
export default function Demo() {
|
||||
const moneyCategoryStore = useMoneyCategoryStore()
|
||||
|
||||
function getCategories() {
|
||||
console.log(moneyCategoryStore.categories)
|
||||
}
|
||||
|
||||
async function resetCategory() {
|
||||
moneyCategoryStore.setCategories([])
|
||||
await moneyCategoryStore.init()
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
className="flex-1"
|
||||
@@ -9,13 +21,16 @@ export default function Demo() {
|
||||
marginTop: 34,
|
||||
}}
|
||||
>
|
||||
<ACellGroup >
|
||||
<ACell leftIcon='Zap' title="测试" rightText="测试右边" isLink />
|
||||
<ACellGroup>
|
||||
<ACell leftIcon="Zap" title="测试" rightText="测试右边" isLink />
|
||||
<ACell leftIcon="Airplay" title="测试" rightText="测试右边" isLink />
|
||||
<ACell leftIcon="Airplay" title="测试" rightText="测试右边" isLink />
|
||||
<ACell leftIcon="Airplay" title="测试" rightText="测试右边" isLink />
|
||||
<ACell leftIcon="Airplay" title="测试" rightText="测试右边" isLink />
|
||||
</ACellGroup>
|
||||
|
||||
<AButton onPress={() => resetCategory()}>重置记账分类</AButton>
|
||||
<AButton onPress={() => getCategories()}>获取记账分类</AButton>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MoneyRecordGroup, MoneyRecordItem } from '@/types/money/record'
|
||||
import React, { useState } from 'react'
|
||||
import { SectionList, Text, TouchableHighlight, View } from 'react-native'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { RefreshControl, SectionList, Text, TouchableHighlight, View } from 'react-native'
|
||||
import {
|
||||
Menu,
|
||||
MenuOption,
|
||||
@@ -11,12 +11,15 @@ import {
|
||||
import { LucideIcon } from '@/components/common/ao'
|
||||
import EmptyState from '@/components/common/EmptyStatus'
|
||||
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||
|
||||
interface StaticTransactionsProps {
|
||||
records: MoneyRecordGroup[]
|
||||
onUpdate?: (data: MoneyRecordItem) => void
|
||||
onDelete?: (id: number) => void
|
||||
onDetail?: (id: number) => void
|
||||
onRefreshData?: () => void
|
||||
onLoadMore?: () => void
|
||||
}
|
||||
|
||||
export default function StaticTransactions({
|
||||
@@ -24,9 +27,45 @@ export default function StaticTransactions({
|
||||
onUpdate,
|
||||
onDelete,
|
||||
onDetail,
|
||||
onLoadMore,
|
||||
onRefreshData,
|
||||
}: StaticTransactionsProps) {
|
||||
const moneyCategoryStore = useMoneyCategoryStore()
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [loadingMore, setLoadingMore] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null)
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
if (!onRefreshData)
|
||||
return
|
||||
setRefreshing(true)
|
||||
try {
|
||||
await onRefreshData()
|
||||
}
|
||||
finally {
|
||||
setRefreshing(false)
|
||||
}
|
||||
}, [onRefreshData])
|
||||
|
||||
const handleLoadMore = useCallback(async () => {
|
||||
if (!onLoadMore || loadingMore)
|
||||
return
|
||||
setLoadingMore(true)
|
||||
try {
|
||||
await onLoadMore()
|
||||
}
|
||||
finally {
|
||||
setLoadingMore(false)
|
||||
}
|
||||
}, [onLoadMore, loadingMore])
|
||||
|
||||
const sections = records.map(group => ({
|
||||
title: group.record_time,
|
||||
totalIncome: group.income,
|
||||
totalExpense: group.expense,
|
||||
data: group.records,
|
||||
}))
|
||||
|
||||
if (records.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
@@ -37,19 +76,17 @@ export default function StaticTransactions({
|
||||
)
|
||||
}
|
||||
|
||||
const sections = records.map(group => ({
|
||||
title: group.record_time,
|
||||
totalIncome: group.income,
|
||||
totalExpense: group.expense,
|
||||
data: group.records,
|
||||
}))
|
||||
|
||||
return (
|
||||
<SectionList
|
||||
sections={sections}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
stickySectionHeadersEnabled={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onEndReached={handleLoadMore}
|
||||
onEndReachedThreshold={0.1}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
||||
}
|
||||
renderSectionHeader={({ section }) => (
|
||||
<View className="bg-white py-2 px-4 border-b border-gray-50">
|
||||
<View className="flex-row items-center justify-between">
|
||||
@@ -82,6 +119,8 @@ export default function StaticTransactions({
|
||||
)}
|
||||
|
||||
renderItem={({ item, index }) => {
|
||||
const category = moneyCategoryStore.getCategoryById(item.category_id)
|
||||
|
||||
return (
|
||||
<Menu
|
||||
renderer={renderers.Popover}
|
||||
@@ -104,12 +143,12 @@ export default function StaticTransactions({
|
||||
}}
|
||||
>
|
||||
<View className="relative w-10 h-10 rounded-full bg-gray-100 items-center justify-center">
|
||||
<MoneyCategoryIcon name={item.category_icon} size={20} color="#000" />
|
||||
<MoneyCategoryIcon name={category?.icon} size={20} color="#000" />
|
||||
</View>
|
||||
|
||||
<View className="flex-1">
|
||||
<Text className="text-gray-800 text-base">
|
||||
{item.remark ? item.remark : item.category_name}
|
||||
{item.remark ? item.remark : category?.name}
|
||||
</Text>
|
||||
{
|
||||
index === 1 && (
|
||||
@@ -125,7 +164,7 @@ export default function StaticTransactions({
|
||||
<Text
|
||||
className="text-sm text-gray-600 text-right"
|
||||
>
|
||||
{item.category_type === 1 ? '+' : '-'}
|
||||
{category?.type === 1 ? '+' : '-'}
|
||||
{' '}
|
||||
{item.amount}
|
||||
</Text>
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function Money() {
|
||||
record_time: getCurrentYearMonth(),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
function getMoneyRecords() {
|
||||
queryMoneyRecordsApi({
|
||||
record_time: searchParams.record_time,
|
||||
}).then((res) => {
|
||||
@@ -39,6 +39,10 @@ export default function Money() {
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getMoneyRecords()
|
||||
}, [searchParams])
|
||||
|
||||
return (
|
||||
@@ -141,6 +145,9 @@ export default function Money() {
|
||||
onDelete={(id) => {
|
||||
moneyRecordStore.deleteMoneyRecord(id)
|
||||
}}
|
||||
onRefreshData={() => {
|
||||
getMoneyRecords()
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ErrorBoundaryProps } from 'expo-router'
|
||||
import { ToastProvider } from '@gluestack-ui/core/toast/creator'
|
||||
import { useFonts } from 'expo-font'
|
||||
import { Stack, useRouter } from 'expo-router'
|
||||
import { Stack, usePathname, useRouter } from 'expo-router'
|
||||
import * as SplashScreen from 'expo-splash-screen'
|
||||
import { StatusBar } from 'expo-status-bar'
|
||||
import { useEffect } from 'react'
|
||||
@@ -12,6 +12,7 @@ import { AButton } from '@/components/common/ao'
|
||||
import EmptyState from '@/components/common/EmptyStatus'
|
||||
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'
|
||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||
import { useNavStore } from '@/store/navigation'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { ToastProviderWrapper } from '@/utils/toast'
|
||||
import '@/styles/global.css'
|
||||
@@ -44,29 +45,32 @@ export default function RootLayout() {
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const moneyCategoryStore = useMoneyCategoryStore()
|
||||
const pathname = usePathname()
|
||||
const navStore = useNavStore()
|
||||
|
||||
const [fontsLoaded] = useFonts({
|
||||
SpaceMono: require('@/static/fonts/SpaceMono-Regular.ttf'),
|
||||
})
|
||||
|
||||
// splash screen 隐藏 & 登录跳转
|
||||
useEffect(() => {
|
||||
if (fontsLoaded) {
|
||||
SplashScreen.hideAsync()
|
||||
navStore.setPathname(pathname)
|
||||
}, [pathname])
|
||||
|
||||
useEffect(() => {
|
||||
if (!fontsLoaded)
|
||||
return
|
||||
|
||||
// 初始化分类
|
||||
moneyCategoryStore.init()
|
||||
|
||||
// 登录跳转
|
||||
if (!userStore.isLoggedIn()) {
|
||||
navStore.navigateToAuth()
|
||||
}
|
||||
}, [fontsLoaded])
|
||||
|
||||
if (!fontsLoaded) {
|
||||
// 字体未加载,显示空白或 Splash
|
||||
return null
|
||||
}
|
||||
|
||||
if (!userStore.isLoggedIn()) {
|
||||
router.replace('/auth/index')
|
||||
return null
|
||||
}
|
||||
|
||||
moneyCategoryStore.init()
|
||||
// 隐藏 splash
|
||||
SplashScreen.hideAsync()
|
||||
}, [fontsLoaded, userStore, moneyCategoryStore, router])
|
||||
|
||||
return (
|
||||
<MenuProvider>
|
||||
|
||||
@@ -83,7 +83,6 @@ export default function Example() {
|
||||
/>
|
||||
|
||||
<View className="flex flex-row gap-2">
|
||||
|
||||
<AButton
|
||||
className="rounded-full"
|
||||
leftIcon={{
|
||||
|
||||
@@ -4,16 +4,18 @@ import { Text, View } from 'react-native'
|
||||
import { AButton, ACell, ACellGroup, AModal } from '@/components/common/ao'
|
||||
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||
import { useMoneyRecordStore } from '@/store/moneyRecord'
|
||||
import { toDateString, toDateTimeString } from '@/utils/time'
|
||||
|
||||
export default function MoneyRecordDetail() {
|
||||
const router = useRouter()
|
||||
const moneyRecordStore = useMoneyRecordStore()
|
||||
|
||||
const moneyCategoryStore = useMoneyCategoryStore()
|
||||
const { id: detailId } = useLocalSearchParams<{ id: string }>()
|
||||
|
||||
const moneryRecord = moneyRecordStore.getMoneyRecordById(Number(detailId))
|
||||
const moneyCategory = moneyCategoryStore.getCategoryById(moneryRecord?.category_id || 0)
|
||||
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
|
||||
@@ -27,12 +29,12 @@ export default function MoneyRecordDetail() {
|
||||
<CustomHeader title="账单详情" />
|
||||
<View className="flex-1">
|
||||
<View className="flex flex-col items-center">
|
||||
<MoneyCategoryIcon name={moneryRecord.category_icon} size={56} />
|
||||
<Text className="mt-1">{moneryRecord.category_name}</Text>
|
||||
<MoneyCategoryIcon name={moneyCategory?.icon} size={56} />
|
||||
<Text className="mt-1">{moneyCategory?.name}</Text>
|
||||
</View>
|
||||
|
||||
<ACellGroup showDivider={false} className="mt-4">
|
||||
<ACell title="类别" rightText={moneryRecord.category_type === 0 ? '支出' : '收入'} />
|
||||
<ACell title="类别" rightText={moneyCategory?.type === 0 ? '支出' : '收入'} />
|
||||
<ACell title="金额" rightText={moneryRecord.amount} />
|
||||
<ACell title="备注" rightText={moneryRecord.remark} />
|
||||
<ACell title="日期" rightText={toDateString(moneryRecord.record_time)} />
|
||||
|
||||
@@ -89,9 +89,6 @@ export default function MoenyRecordEditor() {
|
||||
const result: MoneyRecordItem = {
|
||||
...moneyRecord,
|
||||
category_id: selectedCategory.id,
|
||||
category_name: selectedCategory.name,
|
||||
category_icon: selectedCategory.icon,
|
||||
category_type: selectedCategory.type,
|
||||
}
|
||||
|
||||
moneyRecordStore.updateMoneyRecord(result)
|
||||
@@ -104,17 +101,15 @@ export default function MoenyRecordEditor() {
|
||||
}
|
||||
else {
|
||||
insertMoneyRecordApi({
|
||||
...moneyRecord,
|
||||
amount,
|
||||
category_id: selectedCategory.id,
|
||||
remark: moneyRecord.remark,
|
||||
record_time: moneyRecord.record_time,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.isSuccess) {
|
||||
const result: MoneyRecordItem = {
|
||||
...res.data,
|
||||
category_name: selectedCategory.name,
|
||||
category_icon: selectedCategory.icon,
|
||||
category_type: selectedCategory.type,
|
||||
}
|
||||
|
||||
moneyRecordStore.addMoneyRecord(result)
|
||||
|
||||
@@ -1,14 +1,51 @@
|
||||
import { Text, View } from 'react-native'
|
||||
import { useRouter } from 'expo-router'
|
||||
import { useState } from 'react'
|
||||
import { Text, TouchableOpacity, View } from 'react-native'
|
||||
import { AModal } from '@/components/common/ao'
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
export default function Share() {
|
||||
const router = useRouter()
|
||||
|
||||
const moneyCategoryStore = useMoneyCategoryStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const [showExitModal, setShowExitModal] = useState(false)
|
||||
|
||||
function exitLogin() {
|
||||
setShowExitModal(false)
|
||||
|
||||
moneyCategoryStore.setCategories([])
|
||||
userStore.logout()
|
||||
|
||||
router.navigate('/auth')
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1">
|
||||
<CustomHeader title="设置" />
|
||||
|
||||
<View className="flex-1">
|
||||
<Text>123</Text>
|
||||
<TouchableOpacity
|
||||
className="w-full h-12 items-center justify-center bg-gray-100"
|
||||
style={{
|
||||
backgroundColor: '#d9d9db',
|
||||
}}
|
||||
onPress={() => setShowExitModal(true)}
|
||||
>
|
||||
<Text>退出登录</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<AModal
|
||||
title="确认退出当前账号?"
|
||||
isOpen={showExitModal}
|
||||
onCancel={() => setShowExitModal(false)}
|
||||
onClose={() => setShowExitModal(false)}
|
||||
onConfirm={exitLogin}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,116 @@
|
||||
import type { AFormRef } from '@/components/common/ao'
|
||||
import { SquareUserRound } from 'lucide-react-native'
|
||||
import * as React from 'react'
|
||||
import { View } from 'react-native'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Text, View } from 'react-native'
|
||||
import { AButton, AForm, AFormItem, AInput, ASegmentedTabs, ATab, LucideIcon } from '@/components/common/ao'
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
import { Badge, BadgeIcon, BadgeText } from '@/components/ui/badge'
|
||||
import { Divider } from '@/components/ui/divider'
|
||||
|
||||
export default function PagerWithTab() {
|
||||
const formRef = useRef<AFormRef>(null)
|
||||
const [form, setForm] = useState({
|
||||
code: '',
|
||||
})
|
||||
|
||||
function EnterSapce() {
|
||||
return (
|
||||
<View>
|
||||
<AForm ref={formRef} form={form}>
|
||||
<AFormItem
|
||||
name="username"
|
||||
label="邀请码"
|
||||
>
|
||||
<AInput placeholder="请输入邀请码"></AInput>
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
|
||||
<AButton>加入空间</AButton>
|
||||
<View
|
||||
className="flex-row items-center w-full"
|
||||
style={{
|
||||
marginTop: 16,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Divider />
|
||||
</View>
|
||||
<Text className="mx-3 text-gray-500 text-sm">或</Text>
|
||||
<View className="flex-1">
|
||||
<Divider />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<AButton variant="outline">扫码加入</AButton>
|
||||
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1">
|
||||
<CustomHeader title="共享空间" />
|
||||
<View className="flex-1">
|
||||
{/* <View className="p-4">
|
||||
<View className="flex-col gap-4 justify-center items-center">
|
||||
<LucideIcon icon="Earth" size={48} strokeWidth={1.5} />
|
||||
<Text className="font-semibold text-xl">创建共享空间</Text>
|
||||
<Text className="text-base text-center">创建一个专属空间,邀请朋友加入,共享文件、聊天和协作</Text>
|
||||
<AButton className="w-full">创建我的空间</AButton>
|
||||
</View>
|
||||
<View
|
||||
className="flex-row items-center w-full"
|
||||
style={{
|
||||
marginTop: 16,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Divider />
|
||||
</View>
|
||||
<Text className="mx-3 text-gray-500 text-sm">或</Text>
|
||||
<View className="flex-1">
|
||||
<Divider />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<AButton variant="outline">加入已有空间</AButton>
|
||||
</View> */}
|
||||
|
||||
<View className="p-4">
|
||||
<View>
|
||||
<View className="flex flex-row items-center justify-between">
|
||||
<Text className="font-semibold text-lg">邀请好友加入</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className="flex-row px-2 py-4 mt-4"
|
||||
style={{
|
||||
backgroundColor: '#f8f9fa',
|
||||
}}
|
||||
>
|
||||
<Text>123456</Text>
|
||||
<AButton size="xs">复制代码</AButton>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="mt-8">
|
||||
<View className="flex flex-row items-center justify-between">
|
||||
<Text className="font-semibold text-lg">空间成员</Text>
|
||||
<Badge size="lg" variant="solid" action="muted">
|
||||
<BadgeText>3 人</BadgeText>
|
||||
</Badge>
|
||||
</View>
|
||||
|
||||
<View className="mt-2">
|
||||
<Text>列表</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Whitespace-only changes.
@@ -2,12 +2,15 @@ import type { icons } from 'lucide-react-native'
|
||||
import type { StyleProp, TextStyle, ViewStyle } from 'react-native'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Platform, Pressable, Text, View } from 'react-native'
|
||||
import PagerView from 'react-native-pager-view'
|
||||
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'
|
||||
import LucideIcon from './LucideIcon'
|
||||
|
||||
export interface SegmentedTabItem {
|
||||
key: string
|
||||
label: string
|
||||
icon?: keyof typeof icons
|
||||
content?: React.ReactNode
|
||||
}
|
||||
|
||||
interface SegmentedTabsProps {
|
||||
@@ -61,13 +64,12 @@ export default function SegmentedTabs({
|
||||
const [headerMeasuredHeight, setHeaderMeasuredHeight] = useState(tabHeight)
|
||||
const [ready, setReady] = useState(false)
|
||||
|
||||
const pagerRef = useRef<PagerView>(null)
|
||||
const sliderX = useSharedValue(0)
|
||||
const initialized = useRef(false)
|
||||
|
||||
const actualHeaderWidth = width ?? headerMeasuredWidth
|
||||
const tabWidth = actualHeaderWidth ? actualHeaderWidth / tabs.length : 0
|
||||
|
||||
// 🔹 新逻辑:根据外层实际高度动态决定 slider 高度
|
||||
const sliderHeight = Math.max(headerMeasuredHeight - spacing * 2, 0)
|
||||
const sliderWidth = tabWidth - spacing * 2
|
||||
|
||||
@@ -98,6 +100,13 @@ export default function SegmentedTabs({
|
||||
if (value === undefined)
|
||||
setSelectedIndex(index)
|
||||
onChange?.(index)
|
||||
pagerRef.current?.setPage(index)
|
||||
}
|
||||
|
||||
const handlePageSelected = (e: any) => {
|
||||
const index = e.nativeEvent.position
|
||||
setSelectedIndex(index)
|
||||
onChange?.(index)
|
||||
}
|
||||
|
||||
const handleHeaderLayout = (event: any) => {
|
||||
@@ -109,72 +118,89 @@ export default function SegmentedTabs({
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
className={className}
|
||||
style={[
|
||||
{
|
||||
width,
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#f2f2f2',
|
||||
borderRadius,
|
||||
height: tabHeight,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
style,
|
||||
]}
|
||||
onLayout={handleHeaderLayout}
|
||||
>
|
||||
{ready && (
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
position: 'absolute',
|
||||
top: spacing,
|
||||
left: 0,
|
||||
width: sliderWidth,
|
||||
height: sliderHeight,
|
||||
borderRadius,
|
||||
backgroundColor: sliderColor,
|
||||
...Platform.select({ android: { elevation: 6 } }),
|
||||
},
|
||||
sliderStyle,
|
||||
animatedSliderStyle,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<View style={{ flex: 1 }}>
|
||||
{/* Tab Header */}
|
||||
<View
|
||||
className={className}
|
||||
style={[
|
||||
{
|
||||
width,
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#f2f2f2',
|
||||
borderRadius,
|
||||
height: tabHeight,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
style,
|
||||
]}
|
||||
onLayout={handleHeaderLayout}
|
||||
>
|
||||
{ready && (
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
position: 'absolute',
|
||||
top: spacing,
|
||||
left: 0,
|
||||
width: sliderWidth,
|
||||
height: sliderHeight,
|
||||
borderRadius,
|
||||
backgroundColor: sliderColor,
|
||||
...Platform.select({ android: { elevation: 6 } }),
|
||||
},
|
||||
sliderStyle,
|
||||
animatedSliderStyle,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{tabs.map((tab, i) => {
|
||||
const isActive = i === selectedIndex
|
||||
return (
|
||||
<Pressable
|
||||
key={i}
|
||||
onPress={() => handlePress(i)}
|
||||
style={{
|
||||
width: tabWidth,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
gap: spacing,
|
||||
}}
|
||||
>
|
||||
{tab.icon && (
|
||||
<LucideIcon
|
||||
icon={tab.icon}
|
||||
size={iconSize}
|
||||
color={isActive ? selectedColor : unselectedColor}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
style={[
|
||||
{ color: isActive ? selectedColor : unselectedColor, fontSize, fontWeight: 'bold' },
|
||||
isActive ? selectedTextStyle : unselectedTextStyle,
|
||||
]}
|
||||
{tabs.map((tab, i) => {
|
||||
const isActive = i === selectedIndex
|
||||
return (
|
||||
<Pressable
|
||||
key={tab.key}
|
||||
onPress={() => handlePress(i)}
|
||||
style={{
|
||||
width: tabWidth,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
gap: spacing,
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
{tab.icon && (
|
||||
<LucideIcon
|
||||
icon={tab.icon}
|
||||
size={iconSize}
|
||||
color={isActive ? selectedColor : unselectedColor}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
style={[
|
||||
{ color: isActive ? selectedColor : unselectedColor, fontSize, fontWeight: 'bold' },
|
||||
isActive ? selectedTextStyle : unselectedTextStyle,
|
||||
]}
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* PagerView 内容 */}
|
||||
<PagerView
|
||||
ref={pagerRef}
|
||||
style={{ flex: 1, marginTop: 16 }}
|
||||
initialPage={defaultIndex}
|
||||
onPageSelected={handlePageSelected}
|
||||
>
|
||||
{tabs.map(tab => (
|
||||
<View key={tab.key} style={{ flex: 1 }}>
|
||||
{tab.content}
|
||||
</View>
|
||||
))}
|
||||
</PagerView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +1,34 @@
|
||||
import { useState } from 'react'
|
||||
import { Pressable, Text, View } from 'react-native'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { Dimensions, Pressable, Text, View } from 'react-native'
|
||||
import PagerView from 'react-native-pager-view'
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window')
|
||||
|
||||
interface TabItem {
|
||||
label: string
|
||||
key: string
|
||||
content?: React.ReactNode
|
||||
}
|
||||
|
||||
interface TabProps {
|
||||
data?: TabItem[]
|
||||
defaultIndex?: number
|
||||
onChange?: (index: number) => void
|
||||
tabBarClass?: string
|
||||
tabItemClass?: string
|
||||
activeTabItemClass?: string
|
||||
}
|
||||
|
||||
export default function Tab({ data, defaultIndex = 0, onChange }: TabProps) {
|
||||
export default function Tab({
|
||||
data,
|
||||
defaultIndex = 0,
|
||||
onChange,
|
||||
tabBarClass = 'flex flex-row gap-2 overflow-hidden',
|
||||
tabItemClass = 'items-center justify-center rounded-md px-4 py-2 bg-gray-200',
|
||||
activeTabItemClass = 'items-center justify-center rounded-md px-4 py-2 bg-gray-800',
|
||||
}: TabProps) {
|
||||
const [selectedIndex, setSelectedIndex] = useState(defaultIndex)
|
||||
const pagerRef = useRef<PagerView>(null)
|
||||
|
||||
if (!data || data.length === 0)
|
||||
return null
|
||||
@@ -21,26 +36,47 @@ export default function Tab({ data, defaultIndex = 0, onChange }: TabProps) {
|
||||
const handlePress = (index: number) => {
|
||||
setSelectedIndex(index)
|
||||
onChange?.(index)
|
||||
pagerRef.current?.setPage(index)
|
||||
}
|
||||
|
||||
const onPageSelected = (e: any) => {
|
||||
const index = e.nativeEvent.position
|
||||
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 className="flex-1">
|
||||
{/* Tab Bar */}
|
||||
<View className={tabBarClass}>
|
||||
{data.map((item, index) => {
|
||||
const isActive = index === selectedIndex
|
||||
return (
|
||||
<Pressable
|
||||
key={item.key}
|
||||
onPress={() => handlePress(index)}
|
||||
className={isActive ? activeTabItemClass : tabItemClass}
|
||||
>
|
||||
<Text className={isActive ? 'text-white' : 'text-gray-700'}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
|
||||
<PagerView
|
||||
ref={pagerRef}
|
||||
style={{ flex: 1, marginTop: 16 }}
|
||||
initialPage={defaultIndex}
|
||||
onPageSelected={onPageSelected}
|
||||
>
|
||||
{data.map(item => (
|
||||
<View key={item.key} style={{ flex: 1 }}>
|
||||
{item.content}
|
||||
</View>
|
||||
))}
|
||||
</PagerView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -64,13 +64,12 @@ export default function CustomHeader(props: CustomHeaderProps) {
|
||||
headerShown: true,
|
||||
header: () => (
|
||||
<View
|
||||
className={`flex-row items-center h-24 px-2 ${className}`}
|
||||
className={`flex-row items-center h-24 px-2 ${className} `}
|
||||
style={[
|
||||
{ paddingTop: safeArea ? insets.top : 0 },
|
||||
style,
|
||||
]}
|
||||
>
|
||||
|
||||
{
|
||||
left || (
|
||||
<View className="w-10 h-full justify-center">
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
|
||||
import alovaInstance from '../../index'
|
||||
|
||||
const API_PREFIX = '/money'
|
||||
const API_PREFIX = '/money/record'
|
||||
|
||||
export function queryMoneyRecordsApi(params: QueryMoneyRecordsParams) {
|
||||
return alovaInstance.Post<ResponseData<QueryMoneyRecordsResult>>(`${API_PREFIX}/query`, params)
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { RefreshTokenResult } from '@/types/auth'
|
||||
import { axiosRequestAdapter } from '@alova/adapter-axios'
|
||||
import { createAlova } from 'alova'
|
||||
import { router } from 'expo-router'
|
||||
import { useNavStore } from '@/store/navigation'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { toast } from '@/utils/toast'
|
||||
|
||||
@@ -45,11 +46,9 @@ const alovaInstance = createAlova({
|
||||
isSuccess: resData.code === HttpCode.Success,
|
||||
}
|
||||
|
||||
console.log(resData)
|
||||
|
||||
// Access Token 失效 → 重新登录
|
||||
if (resData.code === HttpCode.Unauthorized) {
|
||||
router.replace('/auth/login')
|
||||
useNavStore.getState().navigateToAuth()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -58,12 +57,12 @@ const alovaInstance = createAlova({
|
||||
try {
|
||||
const refreshToken = useUserStore.getState().userInfo?.refreshToken
|
||||
if (!refreshToken) {
|
||||
router.replace('/auth/login')
|
||||
useNavStore.getState().navigateToAuth()
|
||||
return
|
||||
}
|
||||
|
||||
// 调用刷新接口
|
||||
const res = await alovaInstance.Get<ResponseData<RefreshTokenResult>>('/auth/refreshToken', {
|
||||
const res = await alovaInstance.Get<ResponseData<RefreshTokenResult>>('/auth/refresh/token', {
|
||||
headers: { refreshToken },
|
||||
}).send()
|
||||
|
||||
@@ -146,11 +145,7 @@ const alovaInstance = createAlova({
|
||||
return
|
||||
|
||||
if (!useUserStore.getState().isLoggedIn()) {
|
||||
toast.show({
|
||||
title: '尚未登录,请先登录',
|
||||
})
|
||||
|
||||
router.replace('/auth/login')
|
||||
useNavStore.getState().navigateToAuth()
|
||||
return Promise.reject(new Error('未登录'))
|
||||
}
|
||||
|
||||
|
||||
@@ -42,10 +42,15 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await queryMoneyCategoryApi()
|
||||
if (res.isSuccess) {
|
||||
set({ categories: res.data.categories || [] })
|
||||
}
|
||||
await queryMoneyCategoryApi().then((res) => {
|
||||
if (res.isSuccess) {
|
||||
set({ categories: res.data.categories || [] })
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
}).finally(() => {
|
||||
|
||||
})
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Fetch category failed:', e)
|
||||
@@ -61,41 +66,39 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||
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 => {
|
||||
}).then((res) => {
|
||||
console.log('后端返回:', res)
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
|
||||
// 更新前端 state
|
||||
set({ categories: finalCategories })
|
||||
}
|
||||
,
|
||||
},
|
||||
|
||||
deleteCategoryById: (id: number, type: number) => {
|
||||
deleteCategoryById: async (id: number, type: number) => {
|
||||
const allCategories = get().categories
|
||||
|
||||
// 删除指定 id,同时只处理同类型的数据
|
||||
@@ -112,7 +115,9 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||
set({ categories: finalCategories })
|
||||
|
||||
// 调用删除接口
|
||||
deleteMoneyCategoryApi({ id })
|
||||
await deleteMoneyCategoryApi({ id }).catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
// 同步更新排序给后端,仅同类型
|
||||
const payload = finalCategories
|
||||
@@ -121,7 +126,7 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||
id: item.id,
|
||||
sort_number: item.sort_number,
|
||||
}))
|
||||
updateMoneyCategorySortApi({ data: payload })
|
||||
await updateMoneyCategorySortApi({ data: payload })
|
||||
},
|
||||
|
||||
}),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { create } from 'zustand'
|
||||
import { deleteMoneyRecordApi } from '@/request/api/money/record'
|
||||
import { add, sub } from '@/utils/math'
|
||||
import { isInMonth } from '@/utils/time'
|
||||
import { useMoneyCategoryStore } from './moneyCategory'
|
||||
|
||||
interface MoneyRecordStore {
|
||||
records: MoneyRecordGroup[]
|
||||
@@ -39,15 +40,16 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
if (!isInMonth(data.record_time, prev.currentMonth))
|
||||
return prev
|
||||
|
||||
const category = useMoneyCategoryStore.getState().getCategoryById(data.category_id)
|
||||
const records = [...prev.records]
|
||||
const groupIndex = records.findIndex(g => g.record_time === data.record_time)
|
||||
const amount = data.amount
|
||||
|
||||
if (groupIndex !== -1) {
|
||||
const group = { ...records[groupIndex], records: [data, ...records[groupIndex].records] }
|
||||
if (data.category_type === 0)
|
||||
if (category?.type === 0)
|
||||
group.expense = add(group.expense, amount)
|
||||
if (data.category_type === 1)
|
||||
if (category?.type === 1)
|
||||
group.income = add(group.income, amount)
|
||||
records[groupIndex] = group
|
||||
}
|
||||
@@ -55,8 +57,8 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
const newGroup: MoneyRecordGroup = {
|
||||
record_time: data.record_time,
|
||||
records: [data],
|
||||
expense: data.category_type === 0 ? amount : '0.00',
|
||||
income: data.category_type === 1 ? amount : '0.00',
|
||||
expense: category?.type === 0 ? amount : '0.00',
|
||||
income: category?.type === 1 ? amount : '0.00',
|
||||
}
|
||||
const insertIndex = records.findIndex(g => g.record_time < data.record_time)
|
||||
if (insertIndex === -1)
|
||||
@@ -67,8 +69,8 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
return {
|
||||
...prev,
|
||||
records,
|
||||
total_expense: data.category_type === 0 ? add(prev.total_expense, amount) : prev.total_expense,
|
||||
total_income: data.category_type === 1 ? add(prev.total_income, amount) : prev.total_income,
|
||||
total_expense: category?.type === 0 ? add(prev.total_expense, amount) : prev.total_expense,
|
||||
total_income: category?.type === 1 ? add(prev.total_income, amount) : prev.total_income,
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -80,9 +82,12 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
if (oldGroupIndex === -1)
|
||||
return prev
|
||||
|
||||
const updateRecordCategory = useMoneyCategoryStore.getState().getCategoryById((updated.category_id))
|
||||
|
||||
const oldGroup = { ...records[oldGroupIndex], records: [...records[oldGroupIndex].records] }
|
||||
const oldRecordIndex = oldGroup.records.findIndex(r => r.id === updated.id)
|
||||
const oldRecord = oldGroup.records[oldRecordIndex]
|
||||
const oldRecordCategory = useMoneyCategoryStore.getState().getCategoryById((oldRecord.category_id))
|
||||
|
||||
const oldAmount = oldRecord.amount
|
||||
const newAmount = updated.amount
|
||||
@@ -91,9 +96,9 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
oldGroup.records.splice(oldRecordIndex, 1)
|
||||
let total_expense = prev.total_expense
|
||||
let total_income = prev.total_income
|
||||
if (oldRecord.category_type === 0)
|
||||
if (oldRecordCategory?.type === 0)
|
||||
total_expense = sub(total_expense, oldAmount)
|
||||
if (oldRecord.category_type === 1)
|
||||
if (oldRecordCategory?.type === 1)
|
||||
total_income = sub(total_income, oldAmount)
|
||||
if (oldGroup.records.length === 0)
|
||||
records.splice(oldGroupIndex, 1)
|
||||
@@ -107,9 +112,9 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
const newGroupIndex = records.findIndex(g => g.record_time === updated.record_time)
|
||||
if (newGroupIndex !== -1) {
|
||||
const group = { ...records[newGroupIndex], records: [updated, ...records[newGroupIndex].records] }
|
||||
if (updated.category_type === 0)
|
||||
if (updateRecordCategory?.type === 0)
|
||||
group.expense = add(group.expense, newAmount)
|
||||
if (updated.category_type === 1)
|
||||
if (updateRecordCategory?.type === 1)
|
||||
group.income = add(group.income, newAmount)
|
||||
records[newGroupIndex] = group
|
||||
}
|
||||
@@ -117,8 +122,8 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
const newGroup: MoneyRecordGroup = {
|
||||
record_time: updated.record_time,
|
||||
records: [updated],
|
||||
expense: updated.category_type === 0 ? newAmount : '0.00',
|
||||
income: updated.category_type === 1 ? newAmount : '0.00',
|
||||
expense: updateRecordCategory?.type === 0 ? newAmount : '0.00',
|
||||
income: updateRecordCategory?.type === 1 ? newAmount : '0.00',
|
||||
}
|
||||
const insertIndex = records.findIndex(g => g.record_time < updated.record_time)
|
||||
if (insertIndex === -1)
|
||||
@@ -126,9 +131,9 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
else records.splice(insertIndex, 0, newGroup)
|
||||
}
|
||||
|
||||
if (updated.category_type === 0)
|
||||
if (updateRecordCategory?.type === 0)
|
||||
total_expense = add(total_expense, newAmount)
|
||||
if (updated.category_type === 1)
|
||||
if (updateRecordCategory?.type === 1)
|
||||
total_income = add(total_income, newAmount)
|
||||
|
||||
return { ...prev, records, total_expense, total_income }
|
||||
@@ -145,9 +150,11 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
const index = group.records.findIndex(r => r.id === id)
|
||||
if (index !== -1) {
|
||||
const record = group.records[index]
|
||||
if (record.category_type === 0)
|
||||
const category = useMoneyCategoryStore.getState().getCategoryById(record.category_id)
|
||||
|
||||
if (category?.id === 0)
|
||||
total_expense = sub(total_expense, record.amount)
|
||||
if (record.category_type === 1)
|
||||
if (category?.id === 1)
|
||||
total_income = sub(total_income, record.amount)
|
||||
group.records.splice(index, 1)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { router } from 'expo-router'
|
||||
// store/navigation.ts
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface NavState {
|
||||
pathname: string
|
||||
setPathname: (p: string) => void
|
||||
navigateToAuth: () => void
|
||||
}
|
||||
|
||||
export const useNavStore = create<NavState>(set => ({
|
||||
pathname: '/',
|
||||
setPathname: (p: string) => set({ pathname: p }),
|
||||
navigateToAuth: () => {
|
||||
const pathname = useNavStore.getState().pathname
|
||||
|
||||
if (!pathname.startsWith('/auth')) {
|
||||
router.replace('/auth')
|
||||
}
|
||||
},
|
||||
}))
|
||||
@@ -19,9 +19,7 @@ export interface QueryMoneyRecordsParams {
|
||||
|
||||
// 查询结果的记录
|
||||
export interface MoneyRecordItem extends MoneyRecordBase {
|
||||
category_name: string
|
||||
category_icon: string
|
||||
category_type: number
|
||||
|
||||
}
|
||||
|
||||
// 分组 & 查询结果
|
||||
@@ -39,7 +37,12 @@ export interface QueryMoneyRecordsResult {
|
||||
}
|
||||
|
||||
// 新增请求参数
|
||||
export type InsertMoneyRecordParams = Omit<MoneyRecordBase, 'id' | 'create_time'>
|
||||
export interface InsertMoneyRecordParams {
|
||||
category_id: number
|
||||
amount: string
|
||||
remark: string
|
||||
record_time: string
|
||||
}
|
||||
|
||||
export interface InsertMoneyRecordResult extends MoneyRecordBase {}
|
||||
|
||||
|
||||
@@ -1,170 +1,720 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>DataGrid 编辑 + 多选删除 + 彻底删除</title>
|
||||
<link rel="stylesheet" href="https://www.jeasyui.com/easyui/themes/default/easyui.css">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://www.jeasyui.com/easyui/jquery.easyui.min.js"></script>
|
||||
<script src="https://www.jeasyui.com/easyui/locale/easyui-lang-zh_CN.js"></script>
|
||||
<script src="https://www.jeasyui.com/easyui/datagrid-scrollview.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>共享空间</title>
|
||||
<style>
|
||||
.datagrid-editable-input {
|
||||
width: 100% !important;
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
padding: 20px;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
padding: 30px 20px 20px;
|
||||
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
header p {
|
||||
opacity: 0.9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 10px 0 20px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
padding: 6px 15px;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 20px 30px;
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.space-info {
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.space-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.space-status {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.create-space-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.create-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 20px;
|
||||
font-size: 32px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.create-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.create-desc {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 25px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 15px 30px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 4px 12px rgba(37, 117, 252, 0.3);
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(37, 117, 252, 0.4);
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background: white;
|
||||
color: #2575fc;
|
||||
border: 1px solid #2575fc;
|
||||
border-radius: 12px;
|
||||
padding: 15px 30px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.secondary-btn:hover {
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
.invite-section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e1e5e9;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.invite-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.invite-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.invite-toggle {
|
||||
background: #f0f2f5;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.invite-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.invite-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.invite-link {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: #2575fc;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: #1a68e8;
|
||||
}
|
||||
|
||||
.qrcode-container {
|
||||
text-align: center;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.qrcode {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 8px;
|
||||
margin: 0 auto 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.share-options {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.share-icon {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
background: #f0f2f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
border: 1px solid #e1e5e9;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #2575fc;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 用户列表样式 */
|
||||
.user-list-container {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.user-list-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-count {
|
||||
background: #2575fc;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e1e5e9;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
}
|
||||
|
||||
.user-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.owner-badge {
|
||||
background: #ffd700;
|
||||
color: #333;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.user-action-btn {
|
||||
background: #f0f2f5;
|
||||
border: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.user-action-btn:hover {
|
||||
background: #e1e5e9;
|
||||
}
|
||||
|
||||
.leave-btn {
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.leave-btn:hover {
|
||||
background: #ff5252;
|
||||
}
|
||||
|
||||
.switch-mode {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #2575fc;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>共享空间</h1>
|
||||
<p id="header-subtitle">与朋友共享您的空间</p>
|
||||
<div class="status-indicator">
|
||||
<div class="status-badge" id="status-badge">🔒 未加入任何空间</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<h3>DataGrid 编辑 + 多选删除 + 彻底删除 示例(维护 allRows)</h3>
|
||||
<div class="content" id="main-content">
|
||||
<!-- 主要内容将根据状态动态生成 -->
|
||||
</div>
|
||||
|
||||
<table id="subgrid"></table>
|
||||
|
||||
<div style="margin-top:10px;">
|
||||
<a href="javascript:endEditRow()" class="easyui-linkbutton" iconCls="icon-save">保存当前行</a>
|
||||
<a href="javascript:removeRows()" class="easyui-linkbutton" iconCls="icon-remove">删除所选</a>
|
||||
<a href="javascript:confirmDelete()" class="easyui-linkbutton" iconCls="icon-save">彻底删除</a>
|
||||
<a href="javascript:addRow()" class="easyui-linkbutton" iconCls="icon-add">新增一行</a>
|
||||
<a href="javascript:getData()" class="easyui-linkbutton" iconCls="icon-add">获取数据</a>
|
||||
<footer>
|
||||
<p>共享空间 © 2023 - 保护您的隐私安全</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let editIndex = undefined;
|
||||
let allRows = createMockData(2000);
|
||||
// 应用状态管理
|
||||
const appState = {
|
||||
hasSpace: false, // 用户是否有自己的空间
|
||||
spaceName: "我的共享空间",
|
||||
spaceMembers: [
|
||||
{ id: 1, name: "我自己", role: "创建者", status: "在线", avatar: "我" },
|
||||
{ id: 2, name: "张三", role: "成员", status: "在线", avatar: "张" },
|
||||
{ id: 3, name: "李四", role: "成员", status: "10分钟前活跃", avatar: "李" }
|
||||
],
|
||||
inviteLink: "https://share.space/invite/abc123def456"
|
||||
};
|
||||
|
||||
$('#subgrid').datagrid({
|
||||
idField: 'id',
|
||||
fit: false,
|
||||
height: 400,
|
||||
singleSelect: false,
|
||||
rownumbers: false,
|
||||
autoRowHeight: false,
|
||||
pagination: true,
|
||||
pageSize: 20,
|
||||
view: scrollview,
|
||||
onClickRow: onClickRow,
|
||||
showPageInfo: false,
|
||||
showRefresh: false,
|
||||
columns: [[
|
||||
{
|
||||
field: 'rownumber',
|
||||
title: '',
|
||||
width: 30,
|
||||
align: 'center',
|
||||
formatter: function (value, row, index) {
|
||||
return index + 1;
|
||||
},
|
||||
styler: function () {
|
||||
return `
|
||||
background-color: #efefef;
|
||||
background: -webkit-linear-gradient(top, #F9F9F9 0, #efefef 100%);
|
||||
background: -moz-linear-gradient(top,#F9F9F9 0,#efefef 100%);
|
||||
background: -o-linear-gradient(top,#F9F9F9 0,#efefef 100%);
|
||||
background: linear-gradient(to bottom, #F9F9F9 0, #efefef 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#F9F9F9,endColorstr=#efefef,GradientType=0);
|
||||
padding: 0px
|
||||
`
|
||||
}
|
||||
},
|
||||
{ field: 'ck', checkbox: true },
|
||||
{ field: 'id', title: 'ID', width: 50 },
|
||||
{ field: 'name', title: '姓名', width: 120, editor: 'text' },
|
||||
{ field: 'birth', title: '出生日期', width: 140, editor: 'datebox' },
|
||||
{ field: 'score', title: '成绩', width: 100, editor: 'text' }
|
||||
]],
|
||||
data: allRows
|
||||
// 初始化页面
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
renderContent();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
// 点击行编辑
|
||||
function onClickRow(index) {
|
||||
if (editIndex !== index) {
|
||||
if (endEditRow()) {
|
||||
$('#subgrid').datagrid('beginEdit', index);
|
||||
editIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 渲染内容
|
||||
function renderContent() {
|
||||
const mainContent = document.getElementById('main-content');
|
||||
const statusBadge = document.getElementById('status-badge');
|
||||
const headerSubtitle = document.getElementById('header-subtitle');
|
||||
|
||||
// 保存行
|
||||
function endEditRow() {
|
||||
if (editIndex === undefined) return true;
|
||||
if ($('#subgrid').datagrid('validateRow', editIndex)) {
|
||||
$('#subgrid').datagrid('endEdit', editIndex);
|
||||
editIndex = undefined;
|
||||
return true;
|
||||
if (appState.hasSpace) {
|
||||
// 用户有空间时的界面
|
||||
statusBadge.innerHTML = "🏠 拥有空间";
|
||||
headerSubtitle.textContent = "管理您的共享空间";
|
||||
|
||||
mainContent.innerHTML = `
|
||||
<div class="space-info">
|
||||
<div class="space-name">${appState.spaceName}</div>
|
||||
<div class="space-status">已创建 · ${appState.spaceMembers.length - 1}人已加入</div>
|
||||
</div>
|
||||
|
||||
<div class="invite-section">
|
||||
<div class="invite-header">
|
||||
<div class="invite-title">邀请好友加入</div>
|
||||
<button class="invite-toggle">展开</button>
|
||||
</div>
|
||||
|
||||
<div class="invite-content active">
|
||||
<div class="invite-link">
|
||||
<div class="link-text">${appState.inviteLink}</div>
|
||||
<button class="copy-btn">复制链接</button>
|
||||
</div>
|
||||
|
||||
<div class="qrcode-container">
|
||||
<p>或使用二维码邀请</p>
|
||||
<div class="qrcode">二维码区域</div>
|
||||
</div>
|
||||
|
||||
<div class="share-options">
|
||||
<button class="share-btn">
|
||||
<div class="share-icon">微</div>
|
||||
<span>微信</span>
|
||||
</button>
|
||||
<button class="share-btn">
|
||||
<div class="share-icon">Q</div>
|
||||
<span>QQ</span>
|
||||
</button>
|
||||
<button class="share-btn">
|
||||
<div class="share-icon">链</div>
|
||||
<span>复制链接</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-list-container">
|
||||
<div class="user-list-title">
|
||||
<span>空间成员</span>
|
||||
<span class="user-count">${appState.spaceMembers.length}人</span>
|
||||
</div>
|
||||
<div class="user-list">
|
||||
${appState.spaceMembers.map(member => `
|
||||
<div class="user-item">
|
||||
<div class="user-avatar">${member.avatar}</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name">${member.name} ${member.role === '创建者' ? '<span class="owner-badge">创建者</span>' : ''}</div>
|
||||
<div class="user-role">${member.status}</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
${member.role !== '创建者' ?
|
||||
'<button class="user-action-btn remove-user" data-userid="' + member.id + '" title="移除">❌</button>' :
|
||||
'<button class="user-action-btn" title="设置">⚙️</button>'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="leave-btn" id="delete-space-btn">删除空间</button>
|
||||
|
||||
<div class="switch-mode">
|
||||
<button class="switch-btn" id="switch-to-join">退出并加入其他空间</button>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
$.messager.alert('提示', '请填写完整!');
|
||||
return false;
|
||||
// 用户没有空间时的简洁创建引导界面
|
||||
statusBadge.innerHTML = "🔒 未加入任何空间";
|
||||
headerSubtitle.textContent = "与朋友共享您的空间";
|
||||
|
||||
mainContent.innerHTML = `
|
||||
<div class="create-space-card">
|
||||
<div class="create-icon">🏠</div>
|
||||
<div class="create-title">创建共享空间</div>
|
||||
<div class="create-desc">创建一个专属空间,邀请朋友加入,共享文件、聊天和协作</div>
|
||||
<button class="action-btn" id="create-space-btn">创建我的空间</button>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0; color: #666; font-size: 14px;">
|
||||
或
|
||||
</div>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<button class="secondary-btn" id="join-space-btn">加入已有空间</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 新增行
|
||||
function addRow() {
|
||||
if (!endEditRow()) return;
|
||||
// 渲染加入空间界面
|
||||
function renderJoinSpace() {
|
||||
const mainContent = document.getElementById('main-content');
|
||||
const statusBadge = document.getElementById('status-badge');
|
||||
const headerSubtitle = document.getElementById('header-subtitle');
|
||||
|
||||
let newRow = { id: getNextId(), name: '', birth: '', score: '' };
|
||||
allRows.push(newRow);
|
||||
$('#subgrid').datagrid('loadData', allRows);
|
||||
statusBadge.innerHTML = "🔍 加入空间";
|
||||
headerSubtitle.textContent = "输入邀请信息加入空间";
|
||||
|
||||
let newIndex = allRows.length - 1;
|
||||
$('#subgrid').datagrid('selectRow', newIndex);
|
||||
$('#subgrid').datagrid('beginEdit', newIndex);
|
||||
editIndex = newIndex;
|
||||
$('#subgrid').datagrid('scrollTo', newIndex);
|
||||
mainContent.innerHTML = `
|
||||
<div class="form-group">
|
||||
<label for="invite-code">邀请码</label>
|
||||
<input type="text" id="invite-code" class="form-control" placeholder="请输入6位邀请码">
|
||||
</div>
|
||||
|
||||
<button class="action-btn" id="confirm-join-btn">加入空间</button>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0; color: #666; font-size: 14px;">
|
||||
或
|
||||
</div>
|
||||
|
||||
<button class="secondary-btn">
|
||||
<span>扫码加入</span>
|
||||
</button>
|
||||
|
||||
<div class="switch-mode">
|
||||
<button class="switch-btn" id="back-to-main">返回</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 删除行
|
||||
function removeRows() {
|
||||
let selected = $('#subgrid').datagrid('getChecked');
|
||||
if (selected.length === 0) { $.messager.alert('提示', '请先勾选要删除的行'); return; }
|
||||
let ids = selected.map(r => r.id);
|
||||
allRows = allRows.filter(r => !ids.includes(r.id));
|
||||
$('#subgrid').datagrid('loadData', allRows);
|
||||
editIndex = undefined;
|
||||
}
|
||||
// 设置事件监听器
|
||||
function setupEventListeners() {
|
||||
// 创建空间功能
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target.id === 'create-space-btn') {
|
||||
appState.hasSpace = true;
|
||||
renderContent();
|
||||
}
|
||||
|
||||
function getData() {
|
||||
console.log("===getData===")
|
||||
console.log($('#subgrid').datagrid('getData'));
|
||||
console.log("===getChanges===")
|
||||
console.log($('#subgrid').datagrid('getChanges'));
|
||||
console.log("===allRows===")
|
||||
console.log("===allRows===")
|
||||
}
|
||||
if (e.target.id === 'join-space-btn') {
|
||||
renderJoinSpace();
|
||||
}
|
||||
|
||||
// 打印数据
|
||||
function confirmDelete() { console.log('当前有效数据:', allRows); }
|
||||
if (e.target.id === 'back-to-main') {
|
||||
renderContent();
|
||||
}
|
||||
|
||||
// ID 生成
|
||||
function getNextId() {
|
||||
if (allRows.length === 0) return 1;
|
||||
return Math.max(...allRows.map(r => r.id)) + 1;
|
||||
}
|
||||
if (e.target.id === 'switch-to-join') {
|
||||
if (confirm('确定要删除当前空间并加入其他空间吗?')) {
|
||||
appState.hasSpace = false;
|
||||
renderJoinSpace();
|
||||
}
|
||||
}
|
||||
|
||||
// 生成模拟数据
|
||||
function createMockData(count = 100) {
|
||||
let data = [];
|
||||
for (let i = 1; i <= count; i++) {
|
||||
data.push({
|
||||
id: i,
|
||||
name: "用户" + i,
|
||||
birth: randomDate("1980-01-01", "2005-12-31"),
|
||||
score: Math.floor(Math.random() * 41) + 60
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
if (e.target.id === 'delete-space-btn') {
|
||||
if (confirm('确定要删除这个空间吗?此操作不可撤销。')) {
|
||||
appState.hasSpace = false;
|
||||
renderContent();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 随机日期
|
||||
function randomDate(start, end) {
|
||||
let st = new Date(start).getTime();
|
||||
let et = new Date(end).getTime();
|
||||
let t = st + Math.random() * (et - st);
|
||||
let d = new Date(t);
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
// 复制链接功能
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('copy-btn')) {
|
||||
const linkText = e.target.closest('.invite-link').querySelector('.link-text').textContent;
|
||||
navigator.clipboard.writeText(linkText).then(() => {
|
||||
const originalText = e.target.textContent;
|
||||
e.target.textContent = '已复制';
|
||||
setTimeout(() => {
|
||||
e.target.textContent = originalText;
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 邀请区域展开/收起功能
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('invite-toggle')) {
|
||||
const inviteContent = e.target.closest('.invite-section').querySelector('.invite-content');
|
||||
inviteContent.classList.toggle('active');
|
||||
e.target.textContent = inviteContent.classList.contains('active') ? '收起' : '展开';
|
||||
}
|
||||
});
|
||||
|
||||
// 移除用户功能
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('remove-user')) {
|
||||
const userId = parseInt(e.target.getAttribute('data-userid'));
|
||||
const userName = e.target.closest('.user-item').querySelector('.user-name').textContent.split(' ')[0];
|
||||
|
||||
if (confirm(`确定要移除 ${userName} 吗?`)) {
|
||||
// 从成员列表中移除
|
||||
appState.spaceMembers = appState.spaceMembers.filter(member => member.id !== userId);
|
||||
renderContent();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 加入空间功能
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target.id === 'confirm-join-btn') {
|
||||
const inviteCode = document.getElementById('invite-code').value;
|
||||
if (inviteCode && inviteCode.length === 6) {
|
||||
// 模拟加入空间
|
||||
alert(`成功加入空间!邀请码: ${inviteCode}`);
|
||||
|
||||
// 这里可以添加实际加入空间的逻辑
|
||||
// 例如:appState.hasSpace = true; 并设置空间信息
|
||||
|
||||
// 返回主界面
|
||||
renderContent();
|
||||
} else {
|
||||
alert('请输入有效的6位邀请码');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Binary file not shown.
Reference in new issue
Block a user