mirror of
				https://github.com/EasyTier/EasyTier.git
				synced 2025-11-01 04:22:55 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			507 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			507 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | ||
|   <div>
 | ||
|     <el-form ref="formRef" :model="form" :rules="rules" label-width="120px" label-position="left"
 | ||
|       @submit.prevent="handleSubmit">
 | ||
|       <el-form-item label="节点名称" prop="name" required>
 | ||
|         <el-input v-model="form.name" placeholder="请输入节点名称,如:北京-联通-01" maxlength="100" show-word-limit clearable>
 | ||
|           <template #prefix>
 | ||
|             <el-icon>
 | ||
|               <Monitor />
 | ||
|             </el-icon>
 | ||
|           </template>
 | ||
|         </el-input>
 | ||
|         <div class="form-tip">建议使用地区-运营商-编号的格式命名</div>
 | ||
|       </el-form-item>
 | ||
| 
 | ||
|       <el-row :gutter="20">
 | ||
|         <el-col :span="16">
 | ||
|           <el-form-item label="主机地址" prop="host" required>
 | ||
|             <el-input v-model="form.host" placeholder="请输入IP地址或域名" clearable>
 | ||
|               <template #prefix>
 | ||
|                 <el-icon>
 | ||
|                   <Location />
 | ||
|                 </el-icon>
 | ||
|               </template>
 | ||
|             </el-input>
 | ||
|           </el-form-item>
 | ||
|         </el-col>
 | ||
|         <el-col :span="8">
 | ||
|           <el-form-item label="端口" prop="port" required>
 | ||
|             <el-input-number v-model="form.port" :min="1" :max="65535" placeholder="端口号" style="width: 100%" />
 | ||
|           </el-form-item>
 | ||
|         </el-col>
 | ||
|       </el-row>
 | ||
| 
 | ||
|       <el-form-item label="协议类型" prop="protocol" required>
 | ||
|         <el-radio-group v-model="form.protocol">
 | ||
|           <el-radio value="tcp">TCP</el-radio>
 | ||
|           <el-radio value="udp">UDP</el-radio>
 | ||
|           <el-radio value="ws">WebSocket</el-radio>
 | ||
|           <el-radio value="wss">WebSocket Secure</el-radio>
 | ||
|         </el-radio-group>
 | ||
|         <div class="form-tip">选择节点支持的连接协议</div>
 | ||
|       </el-form-item>
 | ||
| 
 | ||
|       <el-form-item label="允许中转" prop="allow_relay" required>
 | ||
|         <el-radio-group v-model="form.allow_relay">
 | ||
|           <el-radio :value="true">允许中转数据</el-radio>
 | ||
|           <el-radio :value="false">仅用于打洞</el-radio>
 | ||
|         </el-radio-group>
 | ||
|         <div class="form-tip">选择节点是否允许中转其他用户的数据流量</div>
 | ||
|       </el-form-item>
 | ||
| 
 | ||
|       <el-form-item label="网络名称" prop="network_name" required>
 | ||
|         <el-input v-model="form.network_name" placeholder="请输入EasyTier网络名称" maxlength="100" clearable>
 | ||
|           <template #prefix>
 | ||
|             <el-icon>
 | ||
|               <Connection />
 | ||
|             </el-icon>
 | ||
|           </template>
 | ||
|         </el-input>
 | ||
|         <div class="form-tip">与 EasyTier 的 network name 一致,用于后端探活</div>
 | ||
|       </el-form-item>
 | ||
| 
 | ||
|       <el-form-item label="网络密码" prop="network_secret" required>
 | ||
|         <el-input v-model="form.network_secret" type="password" placeholder="请输入网络密码" maxlength="100" clearable
 | ||
|           show-password>
 | ||
|           <template #prefix>
 | ||
|             <el-icon>
 | ||
|               <Lock />
 | ||
|             </el-icon>
 | ||
|           </template>
 | ||
|         </el-input>
 | ||
|         <div class="form-tip">与 EasyTier 的 network secret 一致</div>
 | ||
|       </el-form-item>
 | ||
| 
 | ||
|       <el-form-item label="最大网络数" prop="max_connections" required>
 | ||
|         <el-input-number v-model="form.max_connections" :min="1" :max="10000" placeholder="最大网络数量"
 | ||
|           style="width: 200px" />
 | ||
|         <div class="form-tip">节点能够承载的最大网络数量</div>
 | ||
|       </el-form-item>
 | ||
| 
 | ||
|       <el-form-item label="节点描述" prop="description">
 | ||
|         <el-input v-model="form.description" type="textarea" :rows="4" placeholder="请描述您的节点特点,如:地理位置、网络质量、使用限制等"
 | ||
|           maxlength="500" show-word-limit />
 | ||
|         <div class="form-tip">详细描述有助于用户选择合适的节点</div>
 | ||
|       </el-form-item>
 | ||
| 
 | ||
|       <!-- 联系方式 -->
 | ||
|       <el-form-item label="联系方式" prop="contact_info">
 | ||
|         <div class="contact-section">
 | ||
|           <el-form-item label="微信" prop="wechat">
 | ||
|             <el-input v-model="form.wechat" placeholder="请输入微信号" maxlength="50" clearable>
 | ||
|               <template #prefix>
 | ||
|                 <el-icon>
 | ||
|                   <ChatDotRound />
 | ||
|                 </el-icon>
 | ||
|               </template>
 | ||
|             </el-input>
 | ||
|           </el-form-item>
 | ||
| 
 | ||
|           <el-form-item label="QQ" prop="qq_number">
 | ||
|             <el-input v-model="form.qq_number" placeholder="请输入QQ号" maxlength="20" clearable>
 | ||
|               <template #prefix>
 | ||
|                 <el-icon>
 | ||
|                   <User />
 | ||
|                 </el-icon>
 | ||
|               </template>
 | ||
|             </el-input>
 | ||
|           </el-form-item>
 | ||
| 
 | ||
|           <el-form-item label="邮箱" prop="mail">
 | ||
|             <el-input v-model="form.mail" placeholder="请输入邮箱地址" maxlength="100" clearable>
 | ||
|               <template #prefix>
 | ||
|                 <el-icon>
 | ||
|                   <Message />
 | ||
|                 </el-icon>
 | ||
|               </template>
 | ||
|             </el-input>
 | ||
|           </el-form-item>
 | ||
| 
 | ||
|           <div class="form-tip">请至少填写一种联系方式,便于节点问题时联系您(仅管理员可见)</div>
 | ||
|         </div>
 | ||
|       </el-form-item>
 | ||
| 
 | ||
|       <!-- 连接测试 -->
 | ||
|       <el-form-item label="连接测试">
 | ||
|         <div class="test-section">
 | ||
|           <el-button type="warning" @click="testConnection" :loading="testing" :disabled="!canTest">
 | ||
|             <el-icon>
 | ||
|               <Connection />
 | ||
|             </el-icon>
 | ||
|             测试连接
 | ||
|           </el-button>
 | ||
|           <div v-if="testResult" class="test-result">
 | ||
|             <el-tag :type="testResult.success ? 'success' : 'danger'" size="large">
 | ||
|               {{ testResult.success ? '连接成功' : '连接失败' }}
 | ||
|             </el-tag>
 | ||
|             <span v-if="testResult.message" class="test-message">
 | ||
|               {{ testResult.message }}
 | ||
|             </span>
 | ||
|           </div>
 | ||
|         </div>
 | ||
|         <div class="form-tip">建议在提交前测试连接以确保节点可用</div>
 | ||
|       </el-form-item>
 | ||
| 
 | ||
|       <!-- 使用条款 -->
 | ||
|       <el-form-item prop="agreed" v-if="props.showAgreement">
 | ||
|         <el-checkbox v-model="form.agreed">
 | ||
|           我已阅读并同意
 | ||
|           <el-button type="primary" link @click="showTerms = true">
 | ||
|             《节点共享协议》
 | ||
|           </el-button>
 | ||
|         </el-checkbox>
 | ||
|       </el-form-item>
 | ||
| 
 | ||
|       <!-- 提交按钮 -->
 | ||
|       <el-form-item>
 | ||
|         <div class="submit-section">
 | ||
|           <el-button type="primary" size="large" @click="handleSubmit" :loading="submitting"
 | ||
|             :disabled="!form.agreed && props.showAgreement">
 | ||
|             <el-icon>
 | ||
|               <Upload />
 | ||
|             </el-icon>
 | ||
|             提交节点
 | ||
|           </el-button>
 | ||
|           <el-button size="large" @click="resetFields">
 | ||
|             <el-icon>
 | ||
|               <RefreshLeft />
 | ||
|             </el-icon>
 | ||
|             重置表单
 | ||
|           </el-button>
 | ||
|         </div>
 | ||
|       </el-form-item>
 | ||
|     </el-form> <!-- 使用条款对话框 -->
 | ||
| 
 | ||
|     <el-dialog v-model="showTerms" title="节点共享协议" width="600px">
 | ||
|       <div class="terms-content">
 | ||
|         <h3>1. 节点共享原则</h3>
 | ||
|         <p>• 节点提供者应确保节点的稳定性和可用性</p>
 | ||
|         <p>• 不得利用共享节点进行违法违规活动</p>
 | ||
|         <p>• 尊重其他用户的使用权益</p>
 | ||
| 
 | ||
|         <h3>2. 服务质量要求</h3>
 | ||
|         <p>• 节点应保持7x24小时稳定运行</p>
 | ||
|         <p>• 网络延迟应控制在合理范围内</p>
 | ||
|         <p>• 及时处理连接问题和故障</p>
 | ||
| 
 | ||
|         <h3>3. 数据安全</h3>
 | ||
|         <p>• 不得记录或泄露用户传输数据</p>
 | ||
|         <p>• 保护用户隐私和数据安全</p>
 | ||
|         <p>• 遵守相关法律法规</p>
 | ||
| 
 | ||
|         <h3>4. 免责声明</h3>
 | ||
|         <p>• 平台不对节点服务质量承担责任</p>
 | ||
|         <p>• 用户使用节点服务的风险自担</p>
 | ||
|         <p>• 平台有权移除不符合要求的节点</p>
 | ||
|       </div>
 | ||
| 
 | ||
|       <template #footer>
 | ||
|         <el-button @click="showTerms = false">关闭</el-button>
 | ||
|         <el-button type="primary" @click="acceptTerms">同意并关闭</el-button>
 | ||
|       </template>
 | ||
|     </el-dialog>
 | ||
| 
 | ||
|   </div>
 | ||
| </template>
 | ||
| 
 | ||
| <script setup>
 | ||
| import { ref, reactive, computed, watch } from 'vue'
 | ||
| import {
 | ||
|   Monitor,
 | ||
|   Location,
 | ||
|   PriceTag,
 | ||
|   Connection,
 | ||
|   Upload,
 | ||
|   Edit,
 | ||
|   RefreshLeft,
 | ||
|   ChatDotRound,
 | ||
|   User,
 | ||
|   Message
 | ||
| } from '@element-plus/icons-vue'
 | ||
| import { ElMessage } from 'element-plus'
 | ||
| import { nodeApi } from '../api'
 | ||
| 
 | ||
| const props = defineProps({
 | ||
|   modelValue: {
 | ||
|     type: Object,
 | ||
|     default: () => ({
 | ||
|       name: '',
 | ||
|       host: '',
 | ||
|       port: 11010,
 | ||
|       protocol: 'tcp',
 | ||
|       allow_relay: true,
 | ||
|       network_name: '',
 | ||
|       network_secret: '',
 | ||
|       max_connections: 100,
 | ||
|       description: '',
 | ||
|       wechat: '',
 | ||
|       qq_number: '',
 | ||
|       mail: '',
 | ||
|       agreed: false
 | ||
|     })
 | ||
|   },
 | ||
|   submitting: {
 | ||
|     type: Boolean,
 | ||
|     default: false
 | ||
|   },
 | ||
|   submitText: {
 | ||
|     type: String,
 | ||
|     default: '提交节点'
 | ||
|   },
 | ||
|   submitIcon: {
 | ||
|     type: String,
 | ||
|     default: 'Upload'
 | ||
|   },
 | ||
|   showConnectionTest: {
 | ||
|     type: Boolean,
 | ||
|     default: true
 | ||
|   },
 | ||
|   showAgreement: {
 | ||
|     type: Boolean,
 | ||
|     default: true
 | ||
|   },
 | ||
|   showCancel: {
 | ||
|     type: Boolean,
 | ||
|     default: false
 | ||
|   }
 | ||
| })
 | ||
| 
 | ||
| const emit = defineEmits(['update:modelValue', 'submit', 'reset', 'cancel', 'show-terms'])
 | ||
| 
 | ||
| const formRef = ref()
 | ||
| const testing = ref(false)
 | ||
| const testResult = ref(null)
 | ||
| const showTerms = ref(false)
 | ||
| 
 | ||
| // 表单数据
 | ||
| const form = reactive({ ...props.modelValue })
 | ||
| 
 | ||
| // 监听props变化,更新表单数据
 | ||
| watch(() => props.modelValue, (newValue) => {
 | ||
|   Object.assign(form, newValue)
 | ||
| }, { deep: true })
 | ||
| 
 | ||
| // 监听表单变化,向上传递
 | ||
| watch(form, (newValue) => {
 | ||
|   emit('update:modelValue', { ...newValue })
 | ||
| }, { deep: true })
 | ||
| 
 | ||
| // 表单验证规则
 | ||
| const rules = {
 | ||
|   name: [
 | ||
|     { required: true, message: '请输入节点名称', trigger: 'blur' },
 | ||
|     { min: 1, max: 100, message: '节点名称长度应在1-100个字符之间', trigger: 'blur' }
 | ||
|   ],
 | ||
|   host: [
 | ||
|     { required: true, message: '请输入主机地址', trigger: 'blur' },
 | ||
|     { min: 1, max: 255, message: '主机地址长度应在1-255个字符之间', trigger: 'blur' },
 | ||
|     {
 | ||
|       pattern: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/,
 | ||
|       message: '请输入有效的IP地址或域名',
 | ||
|       trigger: 'blur'
 | ||
|     }
 | ||
|   ],
 | ||
|   port: [
 | ||
|     { required: true, message: '请输入端口号', trigger: 'blur' },
 | ||
|     { type: 'number', min: 1, max: 65535, message: '端口号应在1-65535之间', trigger: 'blur' }
 | ||
|   ],
 | ||
|   protocol: [
 | ||
|     { required: true, message: '请选择协议类型', trigger: 'change' }
 | ||
|   ],
 | ||
|   max_connections: [
 | ||
|     { required: true, message: '请输入最大连接数', trigger: 'blur' },
 | ||
|     { type: 'number', min: 1, max: 10000, message: '最大连接数应在1-10000之间', trigger: 'blur' }
 | ||
|   ],
 | ||
|   version: [
 | ||
|     { max: 50, message: '版本信息长度不能超过50个字符', trigger: 'blur' }
 | ||
|   ],
 | ||
|   description: [
 | ||
|     { max: 500, message: '描述长度不能超过500个字符', trigger: 'blur' }
 | ||
|   ],
 | ||
|   wechat: [
 | ||
|     { max: 50, message: '微信号长度不能超过50个字符', trigger: 'blur' }
 | ||
|   ],
 | ||
|   qq_number: [
 | ||
|     { max: 20, message: 'QQ号长度不能超过20个字符', trigger: 'blur' },
 | ||
|     { pattern: /^[1-9][0-9]{4,19}$/, message: '请输入有效的QQ号', trigger: 'blur' }
 | ||
|   ],
 | ||
|   mail: [
 | ||
|     { max: 100, message: '邮箱地址长度不能超过100个字符', trigger: 'blur' },
 | ||
|     { type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
 | ||
|   ],
 | ||
|   contact_info: [
 | ||
|     {
 | ||
|       validator: (rule, value, callback) => {
 | ||
|         if (!form.wechat && !form.qq_number && !form.mail) {
 | ||
|           callback(new Error('请至少填写一种联系方式'))
 | ||
|         } else {
 | ||
|           callback()
 | ||
|         }
 | ||
|       },
 | ||
|       trigger: 'blur'
 | ||
|     }
 | ||
|   ],
 | ||
|   agreed: [
 | ||
|     {
 | ||
|       validator: (rule, value, callback) => {
 | ||
|         if (!value) {
 | ||
|           callback(new Error('请阅读并同意节点共享协议'))
 | ||
|         } else {
 | ||
|           callback()
 | ||
|         }
 | ||
|       },
 | ||
|       trigger: 'change'
 | ||
|     }
 | ||
|   ]
 | ||
| }
 | ||
| 
 | ||
| // 是否可以测试连接
 | ||
| const canTest = computed(() => {
 | ||
|   return form.host && form.port && form.protocol && form.network_name && form.network_secret
 | ||
| })
 | ||
| 
 | ||
| const buildDataFromForm = () => {
 | ||
|   return {
 | ||
|     name: form.name || 'Test Node',
 | ||
|     host: form.host,
 | ||
|     port: form.port,
 | ||
|     protocol: form.protocol,
 | ||
|     description: form.description || null,
 | ||
|     max_connections: form.max_connections || 100,
 | ||
|     allow_relay: form.allow_relay,
 | ||
|     network_name: form.network_name || null,
 | ||
|     network_secret: form.network_secret || null,
 | ||
|     wechat: form.wechat || null,
 | ||
|     qq_number: form.qq_number || null,
 | ||
|     mail: form.mail || null
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| // 测试连接
 | ||
| const testConnection = async () => {
 | ||
|   if (!canTest.value) {
 | ||
|     ElMessage.warning('请先填写主机地址、端口、协议、网络名称和网络密码')
 | ||
|     return
 | ||
|   }
 | ||
| 
 | ||
|   testing.value = true
 | ||
|   testResult.value = null
 | ||
| 
 | ||
|   try {
 | ||
|     // 构建测试数据
 | ||
|     const testData = buildDataFromForm()
 | ||
| 
 | ||
|     // 调用实际的连接测试API
 | ||
|     const response = await nodeApi.testConnection(testData)
 | ||
| 
 | ||
|     if (response.success) {
 | ||
|       testResult.value = {
 | ||
|         success: true,
 | ||
|         message: '连接测试成功,节点可正常访问'
 | ||
|       }
 | ||
|       ElMessage.success('连接测试成功')
 | ||
|     } else {
 | ||
|       testResult.value = {
 | ||
|         success: false,
 | ||
|         message: response.error || '连接测试失败'
 | ||
|       }
 | ||
|       ElMessage.error('连接测试失败')
 | ||
|     }
 | ||
|   } catch (error) {
 | ||
|     console.error('连接测试失败:', error)
 | ||
|     testResult.value = {
 | ||
|       success: false,
 | ||
|       message: error.response?.data?.error || '测试过程中发生错误,请检查网络连接'
 | ||
|     }
 | ||
|     ElMessage.error('连接测试失败')
 | ||
|   } finally {
 | ||
|     testing.value = false
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| // 提交表单
 | ||
| const handleSubmit = async () => {
 | ||
|   if (!formRef.value) return
 | ||
| 
 | ||
|   try {
 | ||
|     const valid = await formRef.value.validate()
 | ||
|     if (!valid) return
 | ||
| 
 | ||
|     const submitData = buildDataFromForm()
 | ||
| 
 | ||
|     emit('submit', submitData)
 | ||
|   } catch (error) {
 | ||
|     console.error('表单验证失败:', error)
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| // 重置表单
 | ||
| const resetFields = () => {
 | ||
|   if (formRef.value) {
 | ||
|     formRef.value.resetFields()
 | ||
|   }
 | ||
|   testResult.value = null
 | ||
|   emit('reset')
 | ||
| }
 | ||
| 
 | ||
| const acceptTerms = () => {
 | ||
|   form.agreed = true
 | ||
|   showTerms.value = false
 | ||
|   ElMessage.success('已同意节点共享协议')
 | ||
| }
 | ||
| 
 | ||
| // 暴露方法给父组件
 | ||
| defineExpose({
 | ||
|   validate: () => formRef.value?.validate(),
 | ||
|   resetFields: () => formRef.value?.resetFields()
 | ||
| })
 | ||
| </script>
 | ||
| 
 | ||
| <style scoped>
 | ||
| .form-tip {
 | ||
|   font-size: 12px;
 | ||
|   color: #909399;
 | ||
|   margin-top: 4px;
 | ||
| }
 | ||
| 
 | ||
| .test-section {
 | ||
|   display: flex;
 | ||
|   align-items: center;
 | ||
|   gap: 12px;
 | ||
| }
 | ||
| 
 | ||
| .test-result {
 | ||
|   display: flex;
 | ||
|   align-items: center;
 | ||
|   gap: 8px;
 | ||
| }
 | ||
| 
 | ||
| .test-message {
 | ||
|   font-size: 12px;
 | ||
|   color: #606266;
 | ||
| }
 | ||
| 
 | ||
| .submit-section {
 | ||
|   display: flex;
 | ||
|   gap: 12px;
 | ||
| }
 | ||
| 
 | ||
| .contact-section {
 | ||
|   width: 100%;
 | ||
| }
 | ||
| 
 | ||
| .contact-section .el-form-item {
 | ||
|   margin-bottom: 16px;
 | ||
| }
 | ||
| 
 | ||
| .contact-section .el-form-item:last-of-type {
 | ||
|   margin-bottom: 8px;
 | ||
| }
 | ||
| 
 | ||
| .contact-section .el-form-item__label {
 | ||
|   font-size: 14px;
 | ||
|   color: #606266;
 | ||
|   font-weight: 500;
 | ||
| }
 | ||
| </style> | 
