This commit is contained in:
oneao committed 2025-12-01 17:28:59 +08:00
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('&nbsp;')
}
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>
+20 -16
View File
@@ -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 {}
+685 -135
View File
@@ -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.