Commit f58f29e6 authored by 罗超's avatar 罗超

处理冗余内容

parent 6a55551c
Pipeline #350 failed with stages
---
trigger: manual
---
# Frontend Coding Standards
This document defines the coding standards for projects using Vue 3,
TypeScript, Pinia, Vue Router, Acro Design, and Tailwind CSS. These
rules must be followed consistently across the entire codebase.
## 1. Technology Stack
- Vue 3 with Composition API (`<script setup>`)
- TypeScript (strict mode required)
- Vite as the build tool
- Tailwind CSS as the primary styling system
- Acro Design as the UI component library
- Pinia as the state management system
- Vue Router for routing
- VueUse for reactivity utilities
## 2. Code Style and Structure
### 2.1 General Rules
- Code must be concise, maintainable, and technically accurate.
- Use functional and declarative programming patterns; avoid classes.
- Always use named exports for functions.
- Follow the DRY principle; avoid code duplication.
- Use descriptive variable names, including prefixes (is, has, should,
can).
- Maintain a flat and clean folder structure; avoid deeply nested
directories.
### 2.2 File Organization
- Each file contains only related content:
- Components
- Subcomponents
- Helpers
- Static data
- Types
- Directory names must be lowercase with dashes (example:
`components/user-card`).
## 3. TypeScript Standards
### 3.1 Preferred Types
- Use `interface` instead of `type` for data structures.
- Avoid enums; use maps or const objects instead.
- Use `function` keyword for pure functions.
- Provide explicit return types for all exported functions.
### 3.2 Component TypeScript Rules
- Always use `<script setup lang="ts">`.
- Define props with `defineProps` using interfaces.
- Emit events using `defineEmits` with typed definitions.
- Avoid implicit any.
## 4. Vue Component Rules
### 4.1 General
- Components should be functional and composable.
- Keep components small and focused on a single responsibility.
- Use Suspense for async components with a fallback.
- Lazy-load non-critical components.
### 4.2 Styling
- Tailwind CSS is the primary styling solution.
- Prefer utility classes over custom CSS.
- Use mobile-first responsive design.
- Minimize custom CSS.
- Avoid deep selectors and `!important`.
### 4.3 Acro Design Integration
- Use Acro Design components only when needed; prefer Tailwind first.
- Keep UI layout and spacing controlled by Tailwind.
- Use Acro components for forms, tables, overlays, data displays.
## 5. Pinia Standards
### 5.1 Store Design
- Use the Setup Store pattern.
- Each store must be in its own folder under `stores/`.
- Keep state minimal; avoid storing UI-only state unless necessary.
- Use actions for business logic.
- Use getters only for derived states.
### 5.2 Store Structure Example
stores/ user/ index.ts types.ts
### 5.3 Rules
- Do not mutate state outside Pinia actions.
- Store interfaces must be explicitly typed.
- Use persist plugin only when necessary and never for sensitive data.
## 6. Routing Standards
- Use route-based code splitting.
- Define meta fields only when required (auth, layout, title).
- Provide explicit typing for route records.
- Avoid large and monolithic route trees; split into modules.
## 7. VueUse Integration
- Prefer VueUse utilities when applicable.
- Ensure side effects are cleaned up.
- Avoid unnecessary watchers; prefer computed or VueUse helpers.
## 8. Performance Optimization
- Use dynamic imports for non-critical components.
- Avoid unnecessary reactivity.
- Use v-once, v-memo, or shallow refs when beneficial.
- Images must use WebP and lazy loading.
- Use Vite build optimizations.
## 9. Web Vitals and Quality
- Optimize LCP by prioritizing content delivery.
- Avoid layout shift by defining image width and height.
- Ensure minimal blocking JavaScript.
- Test with Lighthouse or WebPageTest.
## 10. Project Structure
Recommended structure:
src/ components/ composables/ stores/ router/ views/ assets/ utils/
types/ styles/ constants/
## 11. Git and Collaboration
- Commit messages must be meaningful.
- No console logs in production.
- All new features must include type definitions.
- Code reviews must enforce these rules.
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
--vt-border-radius-small: 5.525px;
--vt-border-radius-medium: 8.19px;
--vt-border-radius-large: 10.855px;
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* color palette from <https://github.com/vuejs/theme> */
\ No newline at end of file
......@@ -5,33 +5,3 @@
@tailwind components;
@tailwind utilities;
/* Arco Design样式重置 */
.arco-layout {
min-height: 100vh;
}
.arco-layout-sider {
background: #fff !important;
}
.arco-menu {
background: transparent !important;
border: none !important;
}
#app {
font-weight: normal;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
/* 全局响应式优化 */
html {
scroll-behavior: smooth;
}
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin: 0;
padding: 0;
}
/* 全局样式优化 */
/* ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-%我们面为向旅游而生登录到 */
@font-face {
font-family: 'ding';
font-weight: 400;
src:
url('//at.alicdn.com/wf/webfont/MQHUV6e56ce5/qIQP67dys7rw.woff2') format('woff2'),
url('//at.alicdn.com/wf/webfont/MQHUV6e56ce5/YPNa1ORlf9v8.woff') format('woff');
font-display: swap;
}
/* ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-%销售宝 */
@font-face {
font-family: 'aliAppName';
src:
url('//at.alicdn.com/wf/webfont/MQHUV6e56ce5/roOHe73ocxhL.woff2') format('woff2'),
url('//at.alicdn.com/wf/webfont/MQHUV6e56ce5/GvXLMLVNsYzr.woff') format('woff');
font-display: swap;
}
* {
text-rendering: optimizeLegibility;
}
.ding {
font-family: 'ding', sans-serif;
}
.aliAppName {
font-family: 'aliAppName', sans-serif;
}
.nsm7Bb-HzV7m-LgbsSe{
height: 36px !important;
}
/* 响应式容器 */
.container-responsive {
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
}
/* 卡片样式 */
.card {
@apply bg-white rounded-lg shadow-sm border border-gray-200;
}
.card-hover {
@apply card hover:shadow-md transition-shadow duration-200;
}
/* 表单样式优化 */
.form-responsive {
@apply grid grid-cols-1 gap-4;
}
@media (min-width: 768px) {
.form-responsive {
@apply grid-cols-2;
}
}
/* 按钮组 */
.button-group {
@apply flex flex-col sm:flex-row gap-2 sm:gap-4;
}
/* 统计卡片 */
.stats-card {
@apply card p-6 text-center hover:shadow-lg transition-all duration-200;
}
.stats-value {
@apply text-2xl sm:text-3xl font-bold text-gray-900 mb-2;
}
.stats-label {
@apply text-sm text-gray-600;
}
/* 导航菜单优化 */
.nav-item {
@apply flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200;
}
.nav-item-active {
@apply nav-item bg-blue-50 text-blue-700 border-r-2 border-blue-500;
}
.nav-item-inactive {
@apply nav-item text-gray-600 hover:bg-gray-50 hover:text-gray-900;
}
/* 数据表格响应式 */
.table-responsive {
@apply overflow-x-auto;
}
.table-responsive table {
@apply min-w-full divide-y divide-gray-200;
}
/* 移动端适配 */
@media (max-width: 640px) {
.mobile-stack {
@apply flex-col space-y-2 space-x-0;
}
.mobile-full {
@apply w-full;
}
.mobile-hidden {
@apply hidden;
}
.mobile-show {
@apply block sm:hidden;
}
}
/* 桌面端适配 */
@media (min-width: 1024px) {
.desktop-sidebar {
@apply w-64 flex-shrink-0;
}
.desktop-content {
@apply flex-1 min-w-0;
}
}
/* 状态颜色 */
.status-success {
@apply bg-green-100 text-green-800 px-2 py-1 rounded-full text-xs font-medium;
}
.status-warning {
@apply bg-yellow-100 text-yellow-800 px-2 py-1 rounded-full text-xs font-medium;
}
.status-error {
@apply bg-red-100 text-red-800 px-2 py-1 rounded-full text-xs font-medium;
}
.status-info {
@apply bg-blue-100 text-blue-800 px-2 py-1 rounded-full text-xs font-medium;
}
/* 加载状态 */
.loading-skeleton {
@apply animate-pulse bg-gray-200 rounded;
}
/* 空状态 */
.empty-state {
@apply text-center py-12;
}
.empty-state-icon {
@apply w-12 h-12 mx-auto text-gray-400 mb-4;
}
.empty-state-title {
@apply text-lg font-medium text-gray-900 mb-2;
}
.empty-state-description {
@apply text-gray-500;
}
/* 渐变背景 */
.gradient-blue {
@apply bg-gradient-to-r from-blue-500 to-blue-600;
}
.gradient-green {
@apply bg-gradient-to-r from-green-500 to-green-600;
}
.gradient-purple {
@apply bg-gradient-to-r from-purple-500 to-purple-600;
}
.gradient-orange {
@apply bg-gradient-to-r from-orange-500 to-orange-600;
}
/* 文本截断 */
.text-truncate {
@apply truncate;
}
.text-truncate-2 {
@apply overflow-hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* 阴影层级 */
.shadow-card {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
.shadow-card-hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.shadow-modal {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
/* 自定义滚动条 */
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #cbd5e0 #f7fafc;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f7fafc;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #cbd5e0;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: #a0aec0;
}
.vt-rounded,.arco-btn,.arco-input-wrapper,.arco-select-view,.arco-tag,.arco-textarea-wrapper,.arco-picker{
border-radius: var(--vt-border-radius-small) !important;
}
.arco-pagination-item{
border-radius: var(--vt-border-radius-small) !important;
}
.arco-input-prefix *{
border: none !important;
background: transparent !important;
}
.arco-upload-list-picture img{
object-fit: contain !important;
}
\ No newline at end of file
import { useUserStore } from './user'
import { useTableStore } from './table'
export { useUserStore, useTableStore }
export { useUserStore }
import { defineStore } from 'pinia'
/**
* 表格列配置 Store
* 用于保存每个表格的列显示/隐藏配置
*/
export const useTableStore = defineStore('table', {
state: () => ({
// 存储每个表格的列配置
// key: 表格唯一标识符
// value: 隐藏的列的 dataIndex 数组
columnConfigs: {} as Record<string, string[]>
}),
actions: {
/**
* 保存表格列配置
* @param tableKey 表格唯一标识符
* @param hiddenColumns 隐藏的列的 dataIndex 数组
*/
saveColumnConfig(tableKey: string, hiddenColumns: string[]) {
this.columnConfigs[tableKey] = hiddenColumns
},
/**
* 获取表格列配置
* @param tableKey 表格唯一标识符
* @returns 隐藏的列的 dataIndex 数组
*/
getColumnConfig(tableKey: string): string[] {
return this.columnConfigs[tableKey] || []
},
/**
* 清除表格列配置
* @param tableKey 表格唯一标识符
*/
clearColumnConfig(tableKey: string) {
delete this.columnConfigs[tableKey]
}
},
// 使用 localStorage 持久化
persist: true
})
import { ApiResult } from '@/types/ApiResult'
import ErpUserService from '@/services/ErpUserService'
import { ResultMessage } from '@/utils/message'
import { defineStore } from 'pinia'
import { type StorageLike } from 'pinia-plugin-persistedstate'
import SecureLS from 'secure-ls'
const USER_DEFAULT_HEADER =
'https://preview.keenthemes.com/metronic/theme/html/demo1/dist/assets/media/users/300_21.jpg'
const ENTERPRISE_DEFAULT_HEADER =
'https://viitto-1301420277.cos.ap-chengdu.myqcloud.com/Test/Upload/Goods/1714387862000_894.png'
const ls = new SecureLS({
isCompression: false,
encryptionSecret: '38c31684-d00d-30dc-82e0-fad9eec46d1d',
......@@ -23,22 +14,11 @@ const st: StorageLike = {
},
}
export interface UserLoginResult {
status: 'ERROR' | 'CHOSEN' | 'SUCCESS' | 'UNSWEPT' | 'SCANNING'
verify: boolean
data?: any[]
}
export interface AutoLoginResult {
isSuccess: boolean
message: string
}
export const useUserStore = defineStore('user', {
state: () => ({
token: '' as string,
userInfo: {} as any,
denied: false,
menus: [] as any[],
}),
getters: {
getUserToken: (state) => {
......@@ -47,195 +27,10 @@ export const useUserStore = defineStore('user', {
getUser: (state) => {
return state.userInfo
},
getDenied: (state) => {
return state.denied
},
getFlattenMenus: (state) => {
const flattenMenus = [] as any[]
const getChildren = (menus: any[], menuId: number) => {
menus.forEach((menu) => {
const parentId = menuId == 0 ? menu.MenuId : menuId
if (menu.NewChildMenu && menu.NewChildMenu.length > 0) {
getChildren(menu.NewChildMenu, parentId)
} else if (menu.list && menu.list.length > 0) {
getChildren(menu.list, parentId)
} else if (menu.ChildMenu && menu.ChildMenu.length > 0) {
getChildren(menu.ChildMenu, parentId)
} else {
const menuItem = JSON.parse(JSON.stringify(menu))
delete menuItem.NewChildMenu
delete menuItem.ChildMenu
delete menuItem.list
menuItem.ParentId = parentId
if (!menu.MenuUrl.includes('/erp/')) menuItem.MenuUrl = `/crm${menuItem.MenuUrl}`
flattenMenus.push(menuItem)
}
})
}
getChildren(state.menus, 0)
return flattenMenus
},
},
actions: {
async setUserAutoLoginAsync(tempToken: string): Promise<AutoLoginResult> {
try {
const response = await ErpUserService.AutoLoginAsync(tempToken)
if (response.data.resultCode == ApiResult.SUCCESS) {
const d = response.data.data
this.token = d.token
this.userInfo = d.userinfo
if (
!this.userInfo.photo ||
(!this.userInfo.photo.includes('http://') && !this.userInfo.photo.includes('https://'))
) {
this.userInfo.photo = USER_DEFAULT_HEADER
}
if (
!this.userInfo.logo ||
(!this.userInfo.logo.includes('http://') && !this.userInfo.logo.includes('https://'))
) {
this.userInfo.logo = ENTERPRISE_DEFAULT_HEADER
}
return { isSuccess: true, message: '' }
} else {
return { isSuccess: false, message: response.data.message }
}
} catch (error) {
return { isSuccess: false, message: '登录异常,请刷新页面重试' }
}
},
/**
* 谷歌登录
* @param credential 谷歌token
* @returns
*/
async setUserGoogleLoginAsync(credential: string) {
const response = await ErpUserService.GoogleLoginAsync(credential)
if (response.data.resultCode == ApiResult.SUCCESS) {
this.token = response.data.data.token
this.userInfo = response.data.data
return { status: 'SUCCESS', verify: false } as UserLoginResult
} else {
ResultMessage.Error(response.data.message)
return { status: 'ERROR', verify: true } as UserLoginResult
}
},
async setUserLoginOut() {
await ErpUserService.UserSignOutAsync()
this.token = ''
this.userInfo = {}
window.location.href = '/login'
},
setNewUserInfo(user: any) {
this.userInfo = user
},
setToken(token: string) {
this.token = token
},
setOnlyUserInfo(userinfo: any) {
this.userInfo = userinfo
if (
!this.userInfo.photo ||
(!this.userInfo.photo.includes('http://') && !this.userInfo.photo.includes('https://'))
) {
this.userInfo.photo = USER_DEFAULT_HEADER
}
if (
!this.userInfo.logo ||
(!this.userInfo.logo.includes('http://') && !this.userInfo.logo.includes('https://'))
) {
this.userInfo.logo = ENTERPRISE_DEFAULT_HEADER
}
},
async setUserWechatFollowAsync(scene_id: string, islean: any) {
try {
let response = await ErpUserService.WechatLoginAsync(scene_id, islean)
if (response.data.resultCode == ApiResult.SUCCESS) {
return { status: 'SUCCESS' } as UserLoginResult
} else if (response.data.resultCode == ApiResult.SCANNING_STATE)
return { status: 'UNSWEPT' } as UserLoginResult
else if (response.data.resultCode == ApiResult.SCANNING_SUCCESS)
return { status: 'SCANNING' } as UserLoginResult
else if (response.data.resultCode == ApiResult.FAILED)
return { status: 'ERROR' } as UserLoginResult
} catch (error) {}
},
async setUserWechatLoginAsync(scene_id: string, islean: any) {
try {
let response = await ErpUserService.WechatLoginAsync(scene_id, islean)
if (response.data.resultCode == ApiResult.SUCCESS) {
if (response.data.data && response.data.data.token && response.data.data.userinfo) {
this.token = response.data.data.token
this.userInfo = response.data.data.userinfo
if (
!this.userInfo.photo ||
(!this.userInfo.photo.includes('http://') &&
!this.userInfo.photo.includes('https://'))
) {
this.userInfo.photo = USER_DEFAULT_HEADER
}
if (
!this.userInfo.logo ||
(!this.userInfo.logo.includes('http://') && !this.userInfo.logo.includes('https://'))
) {
this.userInfo.logo = ENTERPRISE_DEFAULT_HEADER
}
} else {
return { status: 'ERROR' } as UserLoginResult
}
return { status: 'SUCCESS' } as UserLoginResult
} else if (response.data.resultCode == ApiResult.SCANNING_STATE)
return { status: 'UNSWEPT' } as UserLoginResult
else if (response.data.resultCode == ApiResult.SCANNING_SUCCESS)
return { status: 'SCANNING' } as UserLoginResult
else if (response.data.resultCode == ApiResult.FAILED)
return { status: 'ERROR' } as UserLoginResult
} catch (error) {}
return { status: 'ERROR' } as UserLoginResult
},
async setUserPasswordLoginAsync(account: string, pwd: string, vtoken: string, tid: string) {
try {
let response = await ErpUserService.PasswordLoginAsync(account, pwd,vtoken,tid)
if (response.data.resultCode == ApiResult.SUCCESS) {
this.token = response.data.data.token
this.userInfo = response.data.data
if (
!this.userInfo.Icon ||
(!this.userInfo.Icon.includes('http://') && !this.userInfo.Icon.includes('https://'))
) {
this.userInfo.Icon = USER_DEFAULT_HEADER
}
//await this.getUserMenuPermissionAsync()
return { status: 'SUCCESS', verify: false } as UserLoginResult
} else {
ResultMessage.Error(response.data.message)
return { status: 'ERROR', verify: response.data.data == 1 } as UserLoginResult
}
} catch (error) {}
return { status: 'ERROR', verify: true } as UserLoginResult
},
async getUserMenuPermissionAsync() {
// try {
// let response = await ErpUserService.getUserPermissionAsync()
// if (response.data.resultCode == ApiResult.SUCCESS) {
// this.menus = response.data.data[0].ChildMenu
// }
// } catch (error) {}
},
setOldSaPermission(ia: boolean) {
this.userInfo.ia = ia
this.userInfo.ic = false
},
setUserDeniedStatus(status: boolean) {
this.denied = status
},
logout() {
this.token = ''
this.userInfo = {}
this.menus = []
},
},
persist: {
storage: st,
......
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
/**
* 导出工具函数
* 支持导出为 CSV 和 Excel 格式
*/
import type { TableColumn } from '@/types/table';
/**
* 导出为 CSV 格式
* @param data 数据数组
* @param columns 列配置
* @param filename 文件名
*/
export function exportToCSV(data: any[], columns: TableColumn[], filename: string = 'export.csv') {
// 获取可见的列
const visibleColumns = columns.filter(col =>
col.dataIndex &&
col.dataIndex !== 'action' &&
col.visible !== false
);
// 生成表头
const headers = visibleColumns.map(col => col.title || col.dataIndex).join(',');
// 生成数据行
const rows = data.map(row => {
return visibleColumns.map(col => {
const dataIndex = col.dataIndex as string;
let value = row[dataIndex];
// 如果有格式化函数,使用格式化后的值
if (col.formatter) {
value = col.formatter(value, row);
}
// 处理值中的特殊字符(引号、逗号、换行)
if (value === null || value === undefined) {
return '';
}
const stringValue = String(value);
// 如果包含逗号、引号或换行符,需要用引号包裹,并转义引号
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
return `"${stringValue.replace(/"/g, '""')}"`;
}
return stringValue;
}).join(',');
});
// 合并表头和数据
const csv = [headers, ...rows].join('\n');
// 添加 BOM 以支持 Excel 正确显示中文
const BOM = '\uFEFF';
const csvWithBOM = BOM + csv;
// 创建 Blob 并下载
const blob = new Blob([csvWithBOM], { type: 'text/csv;charset=utf-8;' });
downloadBlob(blob, filename);
}
/**
* 导出为 Excel 格式(HTML Table 方式)
* @param data 数据数组
* @param columns 列配置
* @param filename 文件名
*/
export function exportToExcel(data: any[], columns: TableColumn[], filename: string = 'export.xls') {
// 获取可见的列
const visibleColumns = columns.filter(col =>
col.dataIndex &&
col.dataIndex !== 'action' &&
col.visible !== false
);
// 生成 HTML 表格
let html = `
<html xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns="http://www.w3.org/TR/REC-html40">
<head>
<meta charset="utf-8">
<style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; font-weight: bold; }
</style>
</head>
<body>
<table>
<thead>
<tr>
`;
// 添加表头
visibleColumns.forEach(col => {
html += `<th>${col.title || col.dataIndex}</th>`;
});
html += `
</tr>
</thead>
<tbody>
`;
// 添加数据行
data.forEach(row => {
html += '<tr>';
visibleColumns.forEach(col => {
const dataIndex = col.dataIndex as string;
let value = row[dataIndex];
// 如果有格式化函数,使用格式化后的值
if (col.formatter) {
value = col.formatter(value, row);
}
// 处理空值
if (value === null || value === undefined) {
value = '';
}
// 转义 HTML 特殊字符
const stringValue = String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
html += `<td>${stringValue}</td>`;
});
html += '</tr>';
});
html += `
</tbody>
</table>
</body>
</html>
`;
// 创建 Blob 并下载
const blob = new Blob([html], { type: 'application/vnd.ms-excel' });
downloadBlob(blob, filename);
}
/**
* 下载 Blob 对象
* @param blob Blob 对象
* @param filename 文件名
*/
function downloadBlob(blob: Blob, filename: string) {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
/**
* 格式化日期为文件名友好的格式
* @param date 日期对象
* @returns 格式化后的日期字符串 YYYYMMDD_HHMMSS
*/
export function formatDateForFilename(date: Date = new Date()): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}${month}${day}_${hours}${minutes}${seconds}`;
}
/**
* 导入工具函数
* 支持导入 CSV 和 Excel 格式
*/
import type { TableColumn } from '@/types/table';
/**
* 解析 CSV 文件
* @param file 文件对象
* @param columns 列配置(用于映射字段)
* @returns Promise<数据数组>
*/
export function parseCSV(file: File, columns?: TableColumn[]): Promise<any[]> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const text = e.target?.result as string;
const data = parseCSVText(text, columns);
resolve(data);
} catch (error) {
reject(error);
}
};
reader.onerror = () => {
reject(new Error('读取文件失败'));
};
reader.readAsText(file, 'UTF-8');
});
}
/**
* 解析 CSV 文本
* @param text CSV 文本内容
* @param columns 列配置
* @returns 数据数组
*/
function parseCSVText(text: string, columns?: TableColumn[]): any[] {
// 移除 BOM 标记
if (text.charCodeAt(0) === 0xFEFF) {
text = text.slice(1);
}
const lines = text.split('\n').filter(line => line.trim());
if (lines.length === 0) {
return [];
}
// 解析表头
const headers = parseCSVLine(lines[0]);
// 创建列标题到 dataIndex 的映射
const headerMap: Record<string, string> = {};
if (columns) {
columns.forEach(col => {
if (col.title && col.dataIndex) {
headerMap[col.title] = col.dataIndex as string;
}
});
}
// 解析数据行
const data: any[] = [];
for (let i = 1; i < lines.length; i++) {
const values = parseCSVLine(lines[i]);
if (values.length === 0 || (values.length === 1 && !values[0])) {
continue; // 跳过空行
}
const row: any = {};
headers.forEach((header, index) => {
// 如果有列配置,使用 dataIndex,否则使用原始表头
const key = headerMap[header] || header;
row[key] = values[index] || '';
});
data.push(row);
}
return data;
}
/**
* 解析 CSV 行(处理引号和逗号)
* @param line CSV 行文本
* @returns 值数组
*/
function parseCSVLine(line: string): string[] {
const values: string[] = [];
let currentValue = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
const nextChar = line[i + 1];
if (char === '"') {
if (inQuotes && nextChar === '"') {
// 转义的引号
currentValue += '"';
i++; // 跳过下一个引号
} else {
// 切换引号状态
inQuotes = !inQuotes;
}
} else if (char === ',' && !inQuotes) {
// 字段分隔符
values.push(currentValue.trim());
currentValue = '';
} else {
currentValue += char;
}
}
// 添加最后一个字段
values.push(currentValue.trim());
return values;
}
/**
* 解析 Excel 文件(读取为文本,尝试解析)
* @param file 文件对象
* @param columns 列配置
* @returns Promise<数据数组>
*/
export function parseExcel(file: File, columns?: TableColumn[]): Promise<any[]> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const arrayBuffer = e.target?.result as ArrayBuffer;
// 简单处理:如果是 .xls 格式,尝试作为 HTML 解析
// 如果是 .xlsx 格式,需要更复杂的库支持
if (file.name.endsWith('.xls')) {
const text = new TextDecoder('utf-8').decode(arrayBuffer);
const data = parseHTMLTable(text, columns);
resolve(data);
} else {
reject(new Error('暂不支持 .xlsx 格式,请使用 CSV 或 .xls 格式'));
}
} catch (error) {
reject(error);
}
};
reader.onerror = () => {
reject(new Error('读取文件失败'));
};
reader.readAsArrayBuffer(file);
});
}
/**
* 解析 HTML 表格
* @param html HTML 文本
* @param columns 列配置
* @returns 数据数组
*/
function parseHTMLTable(html: string, columns?: TableColumn[]): any[] {
// 创建临时 DOM 元素
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const table = doc.querySelector('table');
if (!table) {
throw new Error('未找到表格数据');
}
// 解析表头
const headerRow = table.querySelector('thead tr') || table.querySelector('tr');
if (!headerRow) {
throw new Error('未找到表头');
}
const headers = Array.from(headerRow.querySelectorAll('th, td')).map(cell =>
cell.textContent?.trim() || ''
);
// 创建列标题到 dataIndex 的映射
const headerMap: Record<string, string> = {};
if (columns) {
columns.forEach(col => {
if (col.title && col.dataIndex) {
headerMap[col.title] = col.dataIndex as string;
}
});
}
// 解析数据行
const tbody = table.querySelector('tbody') || table;
const rows = Array.from(tbody.querySelectorAll('tr')).slice(
table.querySelector('thead') ? 0 : 1 // 如果没有 thead,跳过第一行(表头)
);
const data: any[] = [];
rows.forEach(row => {
const cells = Array.from(row.querySelectorAll('td'));
if (cells.length === 0) {
return; // 跳过空行
}
const rowData: any = {};
headers.forEach((header, index) => {
const key = headerMap[header] || header;
rowData[key] = cells[index]?.textContent?.trim() || '';
});
data.push(rowData);
});
return data;
}
/**
* 触发文件选择对话框
* @param accept 接受的文件类型
* @returns Promise<File | null>
*/
export function selectFile(accept: string = '.csv,.xls,.xlsx'): Promise<File | null> {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = accept;
input.style.display = 'none';
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
resolve(file || null);
document.body.removeChild(input);
};
input.oncancel = () => {
resolve(null);
document.body.removeChild(input);
};
document.body.appendChild(input);
input.click();
});
}
......@@ -8,7 +8,7 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
......
......@@ -18,7 +18,7 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
......
......@@ -16,7 +16,7 @@ export default defineConfig({
},
},
server: {
port: 8001,
port: 8002,
},
css: {
preprocessorOptions: {
......
......@@ -633,10 +633,10 @@
resolved "https://registry.npmmirror.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz"
integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==
"@esbuild/win32-x64@0.21.5":
"@esbuild/darwin-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz"
integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz"
integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
"@hcaptcha/vue3-hcaptcha@^1.3.0":
version "1.3.0"
......@@ -887,10 +887,10 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@parcel/watcher-win32-x64@2.5.1":
"@parcel/watcher-darwin-arm64@2.5.1":
version "2.5.1"
resolved "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz"
integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==
resolved "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz"
integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==
"@parcel/watcher@^2.4.1":
version "2.5.1"
......@@ -940,15 +940,10 @@
estree-walker "^2.0.2"
picomatch "^4.0.2"
"@rollup/rollup-win32-x64-gnu@4.53.2":
"@rollup/rollup-darwin-arm64@4.53.2":
version "4.53.2"
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz"
integrity sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==
"@rollup/rollup-win32-x64-msvc@4.53.2":
version "4.53.2"
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz"
integrity sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz"
integrity sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==
"@sec-ant/readable-stream@^0.4.1":
version "0.4.1"
......@@ -1833,6 +1828,11 @@ fs-extra@^11.2.0:
jsonfile "^6.0.1"
universalify "^2.0.0"
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz"
......@@ -2665,10 +2665,10 @@ rxjs@^7.4.0:
dependencies:
tslib "^2.1.0"
sass-embedded-win32-x64@1.93.3:
sass-embedded-darwin-arm64@1.93.3:
version "1.93.3"
resolved "https://registry.npmmirror.com/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.93.3.tgz"
integrity sha512-wHFVfxiS9hU/sNk7KReD+lJWRp3R0SLQEX4zfOnRP2zlvI2X4IQR5aZr9GNcuMP6TmNpX0nQPZTegS8+h9RrEg==
resolved "https://registry.npmmirror.com/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.93.3.tgz"
integrity sha512-7zb/hpdMOdKteK17BOyyypemglVURd1Hdz6QGsggy60aUFfptTLQftLRg8r/xh1RbQAUKWFbYTNaM47J9yPxYg==
sass-embedded@*, sass-embedded@^1.93.2:
version "1.93.3"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment