feat(server): 用户管理与变更历史接口;修复 result_revisions.parent_id 从未写入
This commit is contained in:
1 parent
f3813171d3
commit
72462792ec
39 files changed
+10001
-28
No files matched your search
@@ -0,0 +1,529 @@
|
||||
import type {
|
||||
Athlete,
|
||||
AthleteInput,
|
||||
EntryImportResult,
|
||||
Event,
|
||||
EventCategory,
|
||||
EventCategoryInput,
|
||||
EventInput,
|
||||
ScoringProfile,
|
||||
ScoringProfileInput,
|
||||
ScoreChannel,
|
||||
ScoreChannelInput,
|
||||
Sport,
|
||||
SportInput,
|
||||
Team,
|
||||
TeamInput,
|
||||
} from '@/shared'
|
||||
|
||||
/**
|
||||
* 基础数据页的离线示例数据源。
|
||||
*
|
||||
* 后端未就绪(连接失败 / 接口未实现)时页面降级到这里,保证每个分区都能走通
|
||||
* 「列表 → 新增 → 编辑 → 停用/删除 → 整体保存」的完整交互。数据只在内存里,
|
||||
* 刷新即重置;界面必须显式标注「示例数据」。
|
||||
*/
|
||||
|
||||
const TEAM_COLORS = ['#3482FF', '#34D399', '#FBBF24', '#F87171', '#A78BFA', '#22D3EE', '#FB923C']
|
||||
|
||||
const SEED_TEAMS: Team[] = [
|
||||
['JG', '机关本部', '机关'],
|
||||
['F1', '第一分公司', '一分公司'],
|
||||
['F2', '第二分公司', '二分公司'],
|
||||
['F3', '第三分公司', '三分公司'],
|
||||
['F4', '第四分公司', '四分公司'],
|
||||
['F5', '第五分公司', '五分公司'],
|
||||
['F6', '第六分公司', '六分公司'],
|
||||
].map(([code, name, short], index) => ({
|
||||
id: index + 1,
|
||||
code: code ?? '',
|
||||
name: name ?? '',
|
||||
shortName: short ?? null,
|
||||
logoPath: null,
|
||||
color: TEAM_COLORS[index] ?? null,
|
||||
sortOrder: index + 1,
|
||||
active: true,
|
||||
}))
|
||||
|
||||
const SEED_SPORTS: Sport[] = [
|
||||
{ id: 1, code: 'TJ', name: '田径', shortName: '田径', defaultProfileId: 1, sortOrder: 1, active: true },
|
||||
{ id: 2, code: 'QL', name: '球类', shortName: '球类', defaultProfileId: 2, sortOrder: 2, active: true },
|
||||
{ id: 3, code: 'QW', name: '趣味', shortName: '趣味', defaultProfileId: 2, sortOrder: 3, active: true },
|
||||
]
|
||||
|
||||
const SEED_PROFILES: ScoringProfile[] = [
|
||||
{
|
||||
id: 1,
|
||||
code: 'individual',
|
||||
name: '个人项目前六名',
|
||||
tieMode: 'skip',
|
||||
recordBonusMode: 'max',
|
||||
awardRanks: 6,
|
||||
lastPlaceZero: false,
|
||||
rules: [
|
||||
{ rank: 1, points: 10 },
|
||||
{ rank: 2, points: 8 },
|
||||
{ rank: 3, points: 6 },
|
||||
{ rank: 4, points: 3 },
|
||||
{ rank: 5, points: 2 },
|
||||
{ rank: 6, points: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
code: 'team',
|
||||
name: '团体项目前六名',
|
||||
tieMode: 'skip',
|
||||
recordBonusMode: 'max',
|
||||
awardRanks: 6,
|
||||
lastPlaceZero: false,
|
||||
rules: [
|
||||
{ rank: 1, points: 15 },
|
||||
{ rank: 2, points: 10 },
|
||||
{ rank: 3, points: 8 },
|
||||
{ rank: 4, points: 4 },
|
||||
{ rank: 5, points: 2 },
|
||||
{ rank: 6, points: 1 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
interface EventSeed {
|
||||
event: Omit<Event, 'id' | 'categories' | 'channels'>
|
||||
category: string
|
||||
scope: ScoreChannel['scope']
|
||||
basis: ScoreChannel['basis']
|
||||
profileId: number
|
||||
/** 示例报名条数:服务端有报名就不允许删项目 / 整体替换组别,这里同步模拟。 */
|
||||
entryCount: number
|
||||
}
|
||||
|
||||
const SEED_EVENTS: EventSeed[] = [
|
||||
{
|
||||
event: {
|
||||
sportId: 1, code: 'M100', name: '男子 100 米', eventType: 'individual', resultMode: 'auto',
|
||||
resultType: 'time', compare: 'asc', recordEligible: true, scored: true, multiplier: 1,
|
||||
status: 'pending', scheduledAt: null, venue: '田径场', sortOrder: 1, remark: null,
|
||||
},
|
||||
category: '男子组', scope: 'athlete', basis: 'result', profileId: 1,
|
||||
entryCount: 7,
|
||||
},
|
||||
{
|
||||
event: {
|
||||
sportId: 1, code: 'W100', name: '女子 100 米', eventType: 'individual', resultMode: 'auto',
|
||||
resultType: 'time', compare: 'asc', recordEligible: true, scored: true, multiplier: 1,
|
||||
status: 'pending', scheduledAt: null, venue: '田径场', sortOrder: 2, remark: null,
|
||||
},
|
||||
category: '女子组', scope: 'athlete', basis: 'result', profileId: 1,
|
||||
entryCount: 0,
|
||||
},
|
||||
{
|
||||
event: {
|
||||
sportId: 1, code: 'M800', name: '男子 800 米', eventType: 'individual', resultMode: 'auto',
|
||||
resultType: 'time', compare: 'asc', recordEligible: true, scored: true, multiplier: 1,
|
||||
status: 'pending', scheduledAt: null, venue: '田径场', sortOrder: 3, remark: null,
|
||||
},
|
||||
category: '男子组', scope: 'athlete', basis: 'result', profileId: 1,
|
||||
entryCount: 5,
|
||||
},
|
||||
{
|
||||
event: {
|
||||
sportId: 1, code: 'LJ', name: '跳远', eventType: 'individual', resultMode: 'auto',
|
||||
resultType: 'distance', compare: 'desc', recordEligible: true, scored: true, multiplier: 1,
|
||||
status: 'pending', scheduledAt: null, venue: '田径场', sortOrder: 4, remark: null,
|
||||
},
|
||||
category: '男子组', scope: 'athlete', basis: 'result', profileId: 1,
|
||||
entryCount: 0,
|
||||
},
|
||||
{
|
||||
event: {
|
||||
sportId: 3, code: 'PU', name: '引体向上', eventType: 'individual', resultMode: 'auto',
|
||||
resultType: 'count', compare: 'desc', recordEligible: false, scored: true, multiplier: 1,
|
||||
status: 'pending', scheduledAt: null, venue: '体育馆', sortOrder: 5, remark: null,
|
||||
},
|
||||
category: '男子组', scope: 'athlete', basis: 'result', profileId: 1,
|
||||
entryCount: 0,
|
||||
},
|
||||
{
|
||||
event: {
|
||||
sportId: 2, code: 'BB', name: '篮球(团体)', eventType: 'team', resultMode: 'manual',
|
||||
resultType: 'score', compare: 'none', recordEligible: false, scored: true, multiplier: 1,
|
||||
status: 'pending', scheduledAt: null, venue: '体育馆', sortOrder: 6, remark: null,
|
||||
},
|
||||
category: '混合组', scope: 'team', basis: 'manual', profileId: 2,
|
||||
entryCount: 3,
|
||||
},
|
||||
{
|
||||
event: {
|
||||
sportId: 3, code: 'FUN', name: '趣味接力(不计分)', eventType: 'team', resultMode: 'manual',
|
||||
resultType: 'none', compare: 'none', recordEligible: false, scored: false, multiplier: 1,
|
||||
status: 'pending', scheduledAt: null, venue: '体育馆', sortOrder: 7, remark: null,
|
||||
},
|
||||
category: '混合组', scope: 'team', basis: 'manual', profileId: 2,
|
||||
entryCount: 0,
|
||||
},
|
||||
]
|
||||
|
||||
const ATHLETE_NAMES = [
|
||||
'张伟', '李强', '王芳', '刘洋', '陈晨', '赵磊', '孙倩', '周涛',
|
||||
'吴敏', '郑浩', '冯雪', '褚健', '卫东', '蒋楠', '沈莉', '韩旭',
|
||||
'杨帆', '朱琳', '秦峰', '许诺', '何静', '吕明', '施雨', '张晓',
|
||||
]
|
||||
|
||||
/** 报名表模板列,与服务端 importColumns 保持一致(大小写不敏感)。 */
|
||||
const IMPORT_COLUMNS = [
|
||||
'event_code', 'category', 'team_code', 'athlete_name', 'bib', 'seed', 'status', 'member_names',
|
||||
]
|
||||
|
||||
const XLSX_HINT = '示例数据模式无法解析 xlsx:请改用 CSV 模板,或连接后端后重试'
|
||||
|
||||
class MockBaseData {
|
||||
private teams: Team[] = SEED_TEAMS.map((team) => ({ ...team }))
|
||||
private sports: Sport[] = SEED_SPORTS.map((sport) => ({ ...sport }))
|
||||
private profiles: ScoringProfile[] = SEED_PROFILES.map((profile) => ({
|
||||
...profile,
|
||||
rules: profile.rules.map((rule) => ({ ...rule })),
|
||||
}))
|
||||
private events: Event[] = []
|
||||
private categories: EventCategory[] = []
|
||||
private channels: ScoreChannel[] = []
|
||||
private athletes: Athlete[] = []
|
||||
|
||||
private nextTeamId = 100
|
||||
private nextSportId = 100
|
||||
private nextEventId = 1000
|
||||
private nextCategoryId = 1000
|
||||
private nextChannelId = 1000
|
||||
private nextProfileId = 100
|
||||
private nextAthleteId = 1000
|
||||
/** 每个小项的示例报名条数,用于复刻服务端「有报名就不能删项目 / 替换组别」的约束。 */
|
||||
private readonly entryCounts = new Map<number, number>()
|
||||
|
||||
constructor() {
|
||||
SEED_EVENTS.forEach((seed, index) => {
|
||||
const id = 101 + index
|
||||
this.events.push({ ...seed.event, id })
|
||||
this.entryCounts.set(id, seed.entryCount)
|
||||
this.categories.push({
|
||||
id: (this.nextCategoryId += 1),
|
||||
eventId: id,
|
||||
name: seed.category,
|
||||
multiplier: 1,
|
||||
gender: seed.category.startsWith('男子') ? '男' : seed.category.startsWith('女子') ? '女' : null,
|
||||
ageMin: null,
|
||||
ageMax: null,
|
||||
sortOrder: 1,
|
||||
})
|
||||
this.channels.push({
|
||||
id: (this.nextChannelId += 1),
|
||||
eventId: id,
|
||||
scope: seed.scope,
|
||||
basis: seed.basis,
|
||||
profileId: seed.profileId,
|
||||
sortOrder: 1,
|
||||
})
|
||||
})
|
||||
|
||||
this.teams.forEach((team, teamIndex) => {
|
||||
ATHLETE_NAMES.slice(teamIndex * 3, teamIndex * 3 + 3).forEach((name, nameIndex) => {
|
||||
this.athletes.push({
|
||||
id: (this.nextAthleteId += 1),
|
||||
name,
|
||||
gender: nameIndex % 2 === 0 ? '男' : '女',
|
||||
teamId: team.id,
|
||||
employeeNo: `${team.code}-${String(nameIndex + 1).padStart(3, '0')}`,
|
||||
active: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ── 参赛单位 ──────────────────────────────────────────────────────────────
|
||||
|
||||
listTeams(): Team[] {
|
||||
return this.teams.map((team) => ({ ...team }))
|
||||
}
|
||||
|
||||
createTeam(input: TeamInput): Team {
|
||||
if (this.teams.some((team) => team.code === input.code)) throw new Error(`单位代码 ${input.code} 已存在`)
|
||||
const team: Team = { ...input, id: (this.nextTeamId += 1) }
|
||||
this.teams = [...this.teams, team]
|
||||
return { ...team }
|
||||
}
|
||||
|
||||
updateTeam(id: number, input: TeamInput): Team {
|
||||
const index = this.teams.findIndex((team) => team.id === id)
|
||||
if (index < 0) throw new Error(`单位 ${id} 不存在`)
|
||||
if (this.teams.some((team) => team.id !== id && team.code === input.code)) {
|
||||
throw new Error(`单位代码 ${input.code} 已存在`)
|
||||
}
|
||||
const team: Team = { ...input, id }
|
||||
this.teams[index] = team
|
||||
return { ...team }
|
||||
}
|
||||
|
||||
// ── 大项 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
listSports(): Sport[] {
|
||||
return this.sports.map((sport) => ({ ...sport }))
|
||||
}
|
||||
|
||||
createSport(input: SportInput): Sport {
|
||||
if (this.sports.some((sport) => sport.code === input.code)) throw new Error(`大项代码 ${input.code} 已存在`)
|
||||
const sport: Sport = { ...input, id: (this.nextSportId += 1) }
|
||||
this.sports = [...this.sports, sport]
|
||||
return { ...sport }
|
||||
}
|
||||
|
||||
updateSport(id: number, input: SportInput): Sport {
|
||||
const index = this.sports.findIndex((sport) => sport.id === id)
|
||||
if (index < 0) throw new Error(`大项 ${id} 不存在`)
|
||||
const sport: Sport = { ...input, id }
|
||||
this.sports[index] = sport
|
||||
return { ...sport }
|
||||
}
|
||||
|
||||
removeSport(id: number): void {
|
||||
const index = this.sports.findIndex((sport) => sport.id === id)
|
||||
if (index < 0) throw new Error(`大项 ${id} 不存在`)
|
||||
if (this.events.some((event) => event.sportId === id)) throw new Error('该大项下还有小项,请先删除小项')
|
||||
this.sports.splice(index, 1)
|
||||
}
|
||||
|
||||
// ── 小项 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
listEvents(): Event[] {
|
||||
return this.events.map((event) => ({ ...event }))
|
||||
}
|
||||
|
||||
getEvent(id: number): Event {
|
||||
const event = this.events.find((item) => item.id === id)
|
||||
if (event === undefined) throw new Error(`小项 ${id} 不存在`)
|
||||
return {
|
||||
...event,
|
||||
categories: this.categories.filter((item) => item.eventId === id).map((item) => ({ ...item })),
|
||||
channels: this.channels.filter((item) => item.eventId === id).map((item) => ({ ...item })),
|
||||
}
|
||||
}
|
||||
|
||||
createEvent(input: EventInput): Event {
|
||||
if (this.events.some((event) => event.code === input.code)) throw new Error(`小项代码 ${input.code} 已存在`)
|
||||
const id = (this.nextEventId += 1)
|
||||
const event: Event = { ...input, id }
|
||||
this.events = [...this.events, event]
|
||||
this.entryCounts.set(id, 0)
|
||||
return { ...event }
|
||||
}
|
||||
|
||||
updateEvent(id: number, input: EventInput): Event {
|
||||
const index = this.events.findIndex((event) => event.id === id)
|
||||
if (index < 0) throw new Error(`小项 ${id} 不存在`)
|
||||
if (this.events.some((event) => event.id !== id && event.code === input.code)) {
|
||||
throw new Error(`小项代码 ${input.code} 已存在`)
|
||||
}
|
||||
const event: Event = { ...input, id }
|
||||
this.events[index] = event
|
||||
return { ...event }
|
||||
}
|
||||
|
||||
removeEvent(id: number): void {
|
||||
const index = this.events.findIndex((event) => event.id === id)
|
||||
if (index < 0) throw new Error(`小项 ${id} 不存在`)
|
||||
if ((this.entryCounts.get(id) ?? 0) > 0) throw new Error('小项已有报名数据,不能删除')
|
||||
this.events.splice(index, 1)
|
||||
this.entryCounts.delete(id)
|
||||
this.categories = this.categories.filter((item) => item.eventId !== id)
|
||||
this.channels = this.channels.filter((item) => item.eventId !== id)
|
||||
}
|
||||
|
||||
saveCategories(eventId: number, inputs: EventCategoryInput[]): EventCategory[] {
|
||||
this.getEvent(eventId)
|
||||
if ((this.entryCounts.get(eventId) ?? 0) > 0) throw new Error('该项目已有报名数据,不能整体替换组别')
|
||||
const names = new Set<string>()
|
||||
for (const input of inputs) {
|
||||
const name = input.name.trim()
|
||||
if (name === '') throw new Error('组别名称必填')
|
||||
if (names.has(name)) throw new Error(`组别名称重复:${name}`)
|
||||
names.add(name)
|
||||
}
|
||||
this.categories = this.categories.filter((item) => item.eventId !== eventId)
|
||||
for (const input of inputs) {
|
||||
this.categories.push({ ...input, id: (this.nextCategoryId += 1), eventId })
|
||||
}
|
||||
return this.getEvent(eventId).categories ?? []
|
||||
}
|
||||
|
||||
saveChannels(eventId: number, inputs: ScoreChannelInput[]): ScoreChannel[] {
|
||||
this.getEvent(eventId)
|
||||
const combinations = new Set<string>()
|
||||
for (const input of inputs) {
|
||||
const key = `${input.scope}:${input.basis}`
|
||||
if (combinations.has(key)) throw new Error(`同一 scope+basis 只能配置一个通道:${key}`)
|
||||
combinations.add(key)
|
||||
}
|
||||
this.channels = this.channels.filter((item) => item.eventId !== eventId)
|
||||
for (const input of inputs) {
|
||||
this.channels.push({ ...input, id: (this.nextChannelId += 1), eventId })
|
||||
}
|
||||
return this.getEvent(eventId).channels ?? []
|
||||
}
|
||||
|
||||
// ── 积分模板 ──────────────────────────────────────────────────────────────
|
||||
|
||||
listProfiles(): ScoringProfile[] {
|
||||
return this.profiles.map((profile) => ({ ...profile, rules: profile.rules.map((rule) => ({ ...rule })) }))
|
||||
}
|
||||
|
||||
createProfile(input: ScoringProfileInput): ScoringProfile {
|
||||
if (this.profiles.some((profile) => profile.code === input.code)) throw new Error(`模板代码 ${input.code} 已存在`)
|
||||
const profile: ScoringProfile = {
|
||||
...input,
|
||||
id: (this.nextProfileId += 1),
|
||||
rules: input.rules.map((rule) => ({ ...rule })),
|
||||
}
|
||||
this.profiles = [...this.profiles, profile]
|
||||
return { ...profile, rules: profile.rules.map((rule) => ({ ...rule })) }
|
||||
}
|
||||
|
||||
updateProfile(id: number, input: ScoringProfileInput): ScoringProfile {
|
||||
const index = this.profiles.findIndex((profile) => profile.id === id)
|
||||
if (index < 0) throw new Error(`积分模板 ${id} 不存在`)
|
||||
const profile: ScoringProfile = {
|
||||
...input,
|
||||
id,
|
||||
rules: input.rules.map((rule) => ({ ...rule })),
|
||||
}
|
||||
this.profiles[index] = profile
|
||||
return { ...profile, rules: profile.rules.map((rule) => ({ ...rule })) }
|
||||
}
|
||||
|
||||
// ── 运动员 ────────────────────────────────────────────────────────────────
|
||||
|
||||
listAthletes(): Athlete[] {
|
||||
return this.athletes.map((athlete) => ({ ...athlete }))
|
||||
}
|
||||
|
||||
createAthlete(input: AthleteInput): Athlete {
|
||||
const athlete: Athlete = { ...input, id: (this.nextAthleteId += 1) }
|
||||
this.athletes = [...this.athletes, athlete]
|
||||
return { ...athlete }
|
||||
}
|
||||
|
||||
updateAthlete(id: number, input: AthleteInput): Athlete {
|
||||
const index = this.athletes.findIndex((athlete) => athlete.id === id)
|
||||
if (index < 0) throw new Error(`运动员 ${id} 不存在`)
|
||||
const athlete: Athlete = { ...input, id }
|
||||
this.athletes[index] = athlete
|
||||
return { ...athlete }
|
||||
}
|
||||
|
||||
removeAthlete(id: number): void {
|
||||
const index = this.athletes.findIndex((athlete) => athlete.id === id)
|
||||
if (index < 0) throw new Error(`运动员 ${id} 不存在`)
|
||||
this.athletes.splice(index, 1)
|
||||
}
|
||||
|
||||
// ── 报名导入 ──────────────────────────────────────────────────────────────
|
||||
|
||||
async importEntries(file: File): Promise<EntryImportResult> {
|
||||
const extension = file.name.slice(file.name.lastIndexOf('.')).toLowerCase()
|
||||
if (extension !== '.csv') {
|
||||
return { created: 0, updated: 0, skipped: 0, errors: [XLSX_HINT] }
|
||||
}
|
||||
const rows = parseCsv(await file.text())
|
||||
if (rows.length < 2) throw new Error('报名表为空')
|
||||
const header = (rows[0] ?? []).map((cell) => cell.replace(/^\uFEFF/, '').trim().toLowerCase())
|
||||
const index = new Map(header.map((name, position) => [name, position]))
|
||||
for (const column of IMPORT_COLUMNS) {
|
||||
if (!index.has(column)) throw new Error(`报名表缺少列 ${column}`)
|
||||
}
|
||||
|
||||
const result: EntryImportResult = { created: 0, updated: 0, skipped: 0, errors: [] }
|
||||
rows.slice(1).forEach((row, offset) => {
|
||||
const line = offset + 2
|
||||
if (row.every((cell) => cell.trim() === '')) return
|
||||
const cell = (name: string): string => {
|
||||
const position = index.get(name)
|
||||
return position === undefined ? '' : (row[position] ?? '').trim()
|
||||
}
|
||||
const eventCode = cell('event_code')
|
||||
const teamCode = cell('team_code')
|
||||
const categoryName = cell('category')
|
||||
if (eventCode === '' || teamCode === '') {
|
||||
result.skipped += 1
|
||||
result.errors.push(`第 ${line} 行:event_code / team_code 必填`)
|
||||
return
|
||||
}
|
||||
const event = this.events.find((item) => item.code === eventCode)
|
||||
if (event === undefined) {
|
||||
result.skipped += 1
|
||||
result.errors.push(`第 ${line} 行:小项 ${eventCode} 不存在`)
|
||||
return
|
||||
}
|
||||
const team = this.teams.find((item) => item.code === teamCode)
|
||||
if (team === undefined) {
|
||||
result.skipped += 1
|
||||
result.errors.push(`第 ${line} 行:单位 ${teamCode} 不存在`)
|
||||
return
|
||||
}
|
||||
const categories = this.categories.filter((item) => item.eventId === event.id)
|
||||
if (categoryName !== '' && !categories.some((item) => item.name === categoryName)) {
|
||||
result.skipped += 1
|
||||
result.errors.push(`第 ${line} 行:组别 ${categoryName} 不存在`)
|
||||
return
|
||||
}
|
||||
if (categoryName === '' && categories.length === 0) {
|
||||
result.skipped += 1
|
||||
result.errors.push(`第 ${line} 行:小项 ${eventCode} 没有组别`)
|
||||
return
|
||||
}
|
||||
result.created += 1
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/** 够用的 CSV 解析:支持双引号包裹与 `""` 转义,报名表不会出现更复杂的结构。 */
|
||||
function parseCsv(text: string): string[][] {
|
||||
const rows: string[][] = []
|
||||
let row: string[] = []
|
||||
let cell = ''
|
||||
let quoted = false
|
||||
|
||||
for (let index = 0; index < text.length; index += 1) {
|
||||
const char = text[index]
|
||||
if (quoted) {
|
||||
if (char === '"') {
|
||||
if (text[index + 1] === '"') {
|
||||
cell += '"'
|
||||
index += 1
|
||||
} else {
|
||||
quoted = false
|
||||
}
|
||||
} else {
|
||||
cell += char
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (char === '"') {
|
||||
quoted = true
|
||||
} else if (char === ',') {
|
||||
row.push(cell)
|
||||
cell = ''
|
||||
} else if (char === '\n') {
|
||||
row.push(cell)
|
||||
rows.push(row)
|
||||
row = []
|
||||
cell = ''
|
||||
} else if (char !== '\r') {
|
||||
cell += char
|
||||
}
|
||||
}
|
||||
if (cell !== '' || row.length > 0) {
|
||||
row.push(cell)
|
||||
rows.push(row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
export const mockBaseData = new MockBaseData()
|
||||
Reference in new issue
Block a user