Initial commit: One Pipe System

完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sexygoat
2026-01-22 16:35:33 +08:00
commit 222e5bb11a
495 changed files with 145440 additions and 0 deletions

View File

@@ -0,0 +1,389 @@
<template>
<el-dialog
v-model="visible"
:title="title"
:width="width"
:close-on-click-modal="false"
:close-on-press-escape="!uploading"
@closed="handleClosed"
>
<div class="import-dialog-content">
<!-- 下载模板 -->
<div v-if="templateUrl" class="template-section">
<el-alert
title="请先下载导入模板,按照模板格式填写数据"
type="info"
:closable="false"
show-icon
>
<template #default>
<el-button
type="primary"
link
@click="handleDownloadTemplate"
>
<el-icon><Download /></el-icon>
下载模板
</el-button>
</template>
</el-alert>
</div>
<!-- 文件上传 -->
<el-upload
ref="uploadRef"
class="upload-area"
drag
:action="uploadAction"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-progress="handleUploadProgress"
:file-list="fileList"
:limit="1"
:accept="accept"
:auto-upload="false"
:disabled="uploading"
>
<el-icon class="upload-icon"><UploadFilled /></el-icon>
<div class="upload-text">
<div>将文件拖到此处<em>点击上传</em></div>
<div class="upload-tip">只能上传 {{ acceptText }} 文件且不超过 {{ maxSizeMB }}MB</div>
</div>
</el-upload>
<!-- 上传进度 -->
<div v-if="uploading" class="upload-progress">
<el-progress :percentage="uploadProgress" />
<p>正在上传中请稍候...</p>
</div>
<!-- 导入结果 -->
<div v-if="importResult" class="import-result">
<el-alert
:title="importResult.message"
:type="importResult.success ? 'success' : 'error'"
:closable="false"
show-icon
>
<template v-if="importResult.detail" #default>
<div class="result-detail">
<p v-if="importResult.detail.total">总计{{ importResult.detail.total }} </p>
<p v-if="importResult.detail.success">成功{{ importResult.detail.success }} </p>
<p v-if="importResult.detail.failed">失败{{ importResult.detail.failed }} </p>
</div>
</template>
</el-alert>
<!-- 失败详情 -->
<div v-if="importResult.errors && importResult.errors.length > 0" class="error-list">
<el-divider content-position="left">失败详情</el-divider>
<el-scrollbar max-height="200px">
<div
v-for="(error, index) in importResult.errors"
:key="index"
class="error-item"
>
<span class="error-row"> {{ error.row }} </span>
<span class="error-msg">{{ error.message }}</span>
</div>
</el-scrollbar>
</div>
</div>
<!-- 额外表单字段 -->
<el-form
v-if="$slots.form"
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
class="import-form"
>
<slot name="form" :form-data="formData" />
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCancel" :disabled="uploading">
{{ importResult ? '关闭' : '取消' }}
</el-button>
<el-button
v-if="!importResult"
type="primary"
:loading="uploading"
@click="handleConfirm"
>
开始导入
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { FormInstance, FormRules, UploadInstance, UploadUserFile, UploadProgressEvent } from 'element-plus'
import { ElMessage } from 'element-plus'
import { Download, UploadFilled } from '@element-plus/icons-vue'
interface ImportResult {
success: boolean
message: string
detail?: {
total?: number
success?: number
failed?: number
}
errors?: Array<{
row: number
message: string
}>
}
interface Props {
modelValue: boolean
title?: string
width?: string | number
// 模板下载地址
templateUrl?: string
// 文件上传地址(如果使用 action 方式)
uploadAction?: string
// 接受的文件类型
accept?: string
// 最大文件大小MB
maxSize?: number
// 表单验证规则
formRules?: FormRules
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', file: File, formData?: Record<string, any>): Promise<ImportResult> | void
(e: 'cancel'): void
}
const props = withDefaults(defineProps<Props>(), {
title: '导入数据',
width: '600px',
uploadAction: '#',
accept: '.xlsx,.xls',
maxSize: 10
})
const emit = defineEmits<Emits>()
const uploadRef = ref<UploadInstance>()
const formRef = ref<FormInstance>()
const fileList = ref<UploadUserFile[]>([])
const formData = ref<Record<string, any>>({})
const uploading = ref(false)
const uploadProgress = ref(0)
const importResult = ref<ImportResult>()
const visible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
}
})
const maxSizeMB = computed(() => props.maxSize)
const acceptText = computed(() => props.accept.replace(/\./g, '').toUpperCase())
const handleDownloadTemplate = () => {
if (props.templateUrl) {
window.open(props.templateUrl, '_blank')
}
}
const handleBeforeUpload = (file: File) => {
const isValidType = props.accept.split(',').some(type => {
const ext = type.trim()
return file.name.toLowerCase().endsWith(ext)
})
if (!isValidType) {
ElMessage.error(`只能上传 ${acceptText.value} 格式的文件`)
return false
}
const isLtSize = file.size / 1024 / 1024 < props.maxSize
if (!isLtSize) {
ElMessage.error(`文件大小不能超过 ${props.maxSize}MB`)
return false
}
return true
}
const handleUploadProgress = (event: UploadProgressEvent) => {
uploadProgress.value = Math.round(event.percent)
}
const handleUploadSuccess = () => {
uploading.value = false
ElMessage.success('导入成功')
}
const handleUploadError = () => {
uploading.value = false
ElMessage.error('导入失败')
}
const handleConfirm = async () => {
if (fileList.value.length === 0) {
ElMessage.warning('请选择要导入的文件')
return
}
// 验证表单(如果有)
if (formRef.value) {
try {
await formRef.value.validate()
} catch {
ElMessage.warning('请检查表单填写')
return
}
}
const file = fileList.value[0].raw
if (!file) return
uploading.value = true
uploadProgress.value = 0
try {
const result = await emit('confirm', file, formData.value)
if (result) {
importResult.value = result
}
} catch (error: any) {
importResult.value = {
success: false,
message: error.message || '导入失败'
}
} finally {
uploading.value = false
}
}
const handleCancel = () => {
visible.value = false
emit('cancel')
}
const handleClosed = () => {
fileList.value = []
formRef.value?.resetFields()
formData.value = {}
uploadProgress.value = 0
importResult.value = undefined
}
defineExpose({
formData
})
</script>
<style scoped lang="scss">
.import-dialog-content {
display: flex;
flex-direction: column;
gap: 16px;
.template-section {
margin-bottom: 8px;
}
.upload-area {
:deep(.el-upload) {
width: 100%;
}
:deep(.el-upload-dragger) {
padding: 40px 20px;
}
.upload-icon {
font-size: 48px;
color: var(--el-color-primary);
margin-bottom: 16px;
}
.upload-text {
font-size: 14px;
color: var(--el-text-color-regular);
em {
color: var(--el-color-primary);
font-style: normal;
}
.upload-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-top: 8px;
}
}
}
.upload-progress {
text-align: center;
padding: 20px 0;
p {
margin-top: 12px;
font-size: 14px;
color: var(--el-text-color-secondary);
}
}
.import-result {
.result-detail {
margin-top: 8px;
font-size: 14px;
line-height: 1.6;
p {
margin: 4px 0;
}
}
.error-list {
margin-top: 16px;
.error-item {
padding: 8px 12px;
font-size: 13px;
line-height: 1.6;
background-color: var(--el-fill-color-light);
border-radius: 4px;
margin-bottom: 8px;
.error-row {
font-weight: 600;
color: var(--el-color-danger);
}
.error-msg {
color: var(--el-text-color-regular);
}
}
}
}
.import-form {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--el-border-color-lighter);
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>