feat(admin): 代理密码可见性 + 复制代理 URL 功能
- 新增 AdminProxy / AdminProxyWithAccountCount DTO,遵循项目 Admin DTO 分层模式 - Proxy.Password 恢复 json:"-" 隐藏,ProxyFromService 不再赋值密码(纵深防御) - 管理员接口使用 ProxyFromServiceAdmin / ProxyWithAccountCountFromServiceAdmin - 前端代理列表新增 Auth 列:显示用户名 + 掩码密码 + 眼睛图标切换可见性 - Address 列新增复制按钮:左键复制完整 URL,右键选择格式 - 编辑模态框密码预填充 + 脏标记,避免误更新
This commit is contained in:
@@ -64,9 +64,9 @@ func (h *ProxyHandler) List(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
out := make([]dto.ProxyWithAccountCount, 0, len(proxies))
|
out := make([]dto.AdminProxyWithAccountCount, 0, len(proxies))
|
||||||
for i := range proxies {
|
for i := range proxies {
|
||||||
out = append(out, *dto.ProxyWithAccountCountFromService(&proxies[i]))
|
out = append(out, *dto.ProxyWithAccountCountFromServiceAdmin(&proxies[i]))
|
||||||
}
|
}
|
||||||
response.Paginated(c, out, total, page, pageSize)
|
response.Paginated(c, out, total, page, pageSize)
|
||||||
}
|
}
|
||||||
@@ -83,9 +83,9 @@ func (h *ProxyHandler) GetAll(c *gin.Context) {
|
|||||||
response.ErrorFrom(c, err)
|
response.ErrorFrom(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
out := make([]dto.ProxyWithAccountCount, 0, len(proxies))
|
out := make([]dto.AdminProxyWithAccountCount, 0, len(proxies))
|
||||||
for i := range proxies {
|
for i := range proxies {
|
||||||
out = append(out, *dto.ProxyWithAccountCountFromService(&proxies[i]))
|
out = append(out, *dto.ProxyWithAccountCountFromServiceAdmin(&proxies[i]))
|
||||||
}
|
}
|
||||||
response.Success(c, out)
|
response.Success(c, out)
|
||||||
return
|
return
|
||||||
@@ -97,9 +97,9 @@ func (h *ProxyHandler) GetAll(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
out := make([]dto.Proxy, 0, len(proxies))
|
out := make([]dto.AdminProxy, 0, len(proxies))
|
||||||
for i := range proxies {
|
for i := range proxies {
|
||||||
out = append(out, *dto.ProxyFromService(&proxies[i]))
|
out = append(out, *dto.ProxyFromServiceAdmin(&proxies[i]))
|
||||||
}
|
}
|
||||||
response.Success(c, out)
|
response.Success(c, out)
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ func (h *ProxyHandler) GetByID(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, dto.ProxyFromService(proxy))
|
response.Success(c, dto.ProxyFromServiceAdmin(proxy))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create handles creating a new proxy
|
// Create handles creating a new proxy
|
||||||
@@ -143,7 +143,7 @@ func (h *ProxyHandler) Create(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return dto.ProxyFromService(proxy), nil
|
return dto.ProxyFromServiceAdmin(proxy), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ func (h *ProxyHandler) Update(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, dto.ProxyFromService(proxy))
|
response.Success(c, dto.ProxyFromServiceAdmin(proxy))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete handles deleting a proxy
|
// Delete handles deleting a proxy
|
||||||
|
|||||||
@@ -293,7 +293,6 @@ func ProxyFromService(p *service.Proxy) *Proxy {
|
|||||||
Host: p.Host,
|
Host: p.Host,
|
||||||
Port: p.Port,
|
Port: p.Port,
|
||||||
Username: p.Username,
|
Username: p.Username,
|
||||||
Password: p.Password,
|
|
||||||
Status: p.Status,
|
Status: p.Status,
|
||||||
CreatedAt: p.CreatedAt,
|
CreatedAt: p.CreatedAt,
|
||||||
UpdatedAt: p.UpdatedAt,
|
UpdatedAt: p.UpdatedAt,
|
||||||
@@ -323,6 +322,51 @@ func ProxyWithAccountCountFromService(p *service.ProxyWithAccountCount) *ProxyWi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProxyFromServiceAdmin converts a service Proxy to AdminProxy DTO for admin users.
|
||||||
|
// It includes the password field - user-facing endpoints must not use this.
|
||||||
|
func ProxyFromServiceAdmin(p *service.Proxy) *AdminProxy {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
base := ProxyFromService(p)
|
||||||
|
if base == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &AdminProxy{
|
||||||
|
Proxy: *base,
|
||||||
|
Password: p.Password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyWithAccountCountFromServiceAdmin converts a service ProxyWithAccountCount to AdminProxyWithAccountCount DTO.
|
||||||
|
// It includes the password field - user-facing endpoints must not use this.
|
||||||
|
func ProxyWithAccountCountFromServiceAdmin(p *service.ProxyWithAccountCount) *AdminProxyWithAccountCount {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
admin := ProxyFromServiceAdmin(&p.Proxy)
|
||||||
|
if admin == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &AdminProxyWithAccountCount{
|
||||||
|
AdminProxy: *admin,
|
||||||
|
AccountCount: p.AccountCount,
|
||||||
|
LatencyMs: p.LatencyMs,
|
||||||
|
LatencyStatus: p.LatencyStatus,
|
||||||
|
LatencyMessage: p.LatencyMessage,
|
||||||
|
IPAddress: p.IPAddress,
|
||||||
|
Country: p.Country,
|
||||||
|
CountryCode: p.CountryCode,
|
||||||
|
Region: p.Region,
|
||||||
|
City: p.City,
|
||||||
|
QualityStatus: p.QualityStatus,
|
||||||
|
QualityScore: p.QualityScore,
|
||||||
|
QualityGrade: p.QualityGrade,
|
||||||
|
QualitySummary: p.QualitySummary,
|
||||||
|
QualityChecked: p.QualityChecked,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ProxyAccountSummaryFromService(a *service.ProxyAccountSummary) *ProxyAccountSummary {
|
func ProxyAccountSummaryFromService(a *service.ProxyAccountSummary) *ProxyAccountSummary {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -221,6 +221,32 @@ type ProxyWithAccountCount struct {
|
|||||||
QualityChecked *int64 `json:"quality_checked,omitempty"`
|
QualityChecked *int64 `json:"quality_checked,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminProxy 是管理员接口使用的 proxy DTO(包含密码等敏感字段)。
|
||||||
|
// 注意:普通接口不得使用此 DTO。
|
||||||
|
type AdminProxy struct {
|
||||||
|
Proxy
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminProxyWithAccountCount 是管理员接口使用的带账号统计的 proxy DTO。
|
||||||
|
type AdminProxyWithAccountCount struct {
|
||||||
|
AdminProxy
|
||||||
|
AccountCount int64 `json:"account_count"`
|
||||||
|
LatencyMs *int64 `json:"latency_ms,omitempty"`
|
||||||
|
LatencyStatus string `json:"latency_status,omitempty"`
|
||||||
|
LatencyMessage string `json:"latency_message,omitempty"`
|
||||||
|
IPAddress string `json:"ip_address,omitempty"`
|
||||||
|
Country string `json:"country,omitempty"`
|
||||||
|
CountryCode string `json:"country_code,omitempty"`
|
||||||
|
Region string `json:"region,omitempty"`
|
||||||
|
City string `json:"city,omitempty"`
|
||||||
|
QualityStatus string `json:"quality_status,omitempty"`
|
||||||
|
QualityScore *int `json:"quality_score,omitempty"`
|
||||||
|
QualityGrade string `json:"quality_grade,omitempty"`
|
||||||
|
QualitySummary string `json:"quality_summary,omitempty"`
|
||||||
|
QualityChecked *int64 `json:"quality_checked,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type ProxyAccountSummary struct {
|
type ProxyAccountSummary struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
@@ -2345,6 +2345,8 @@ export default {
|
|||||||
dataExportConfirm: 'Confirm Export',
|
dataExportConfirm: 'Confirm Export',
|
||||||
dataExported: 'Data exported successfully',
|
dataExported: 'Data exported successfully',
|
||||||
dataExportFailed: 'Failed to export data',
|
dataExportFailed: 'Failed to export data',
|
||||||
|
copyProxyUrl: 'Copy Proxy URL',
|
||||||
|
urlCopied: 'Proxy URL copied',
|
||||||
searchProxies: 'Search proxies...',
|
searchProxies: 'Search proxies...',
|
||||||
allProtocols: 'All Protocols',
|
allProtocols: 'All Protocols',
|
||||||
allStatus: 'All Status',
|
allStatus: 'All Status',
|
||||||
@@ -2358,6 +2360,7 @@ export default {
|
|||||||
name: 'Name',
|
name: 'Name',
|
||||||
protocol: 'Protocol',
|
protocol: 'Protocol',
|
||||||
address: 'Address',
|
address: 'Address',
|
||||||
|
auth: 'Auth',
|
||||||
location: 'Location',
|
location: 'Location',
|
||||||
status: 'Status',
|
status: 'Status',
|
||||||
accounts: 'Accounts',
|
accounts: 'Accounts',
|
||||||
|
|||||||
@@ -2459,6 +2459,7 @@ export default {
|
|||||||
name: '名称',
|
name: '名称',
|
||||||
protocol: '协议',
|
protocol: '协议',
|
||||||
address: '地址',
|
address: '地址',
|
||||||
|
auth: '认证',
|
||||||
location: '地理位置',
|
location: '地理位置',
|
||||||
status: '状态',
|
status: '状态',
|
||||||
accounts: '账号数',
|
accounts: '账号数',
|
||||||
@@ -2486,6 +2487,8 @@ export default {
|
|||||||
allStatuses: '全部状态'
|
allStatuses: '全部状态'
|
||||||
},
|
},
|
||||||
// Additional keys used in ProxiesView
|
// Additional keys used in ProxiesView
|
||||||
|
copyProxyUrl: '复制代理 URL',
|
||||||
|
urlCopied: '代理 URL 已复制',
|
||||||
allProtocols: '全部协议',
|
allProtocols: '全部协议',
|
||||||
allStatus: '全部状态',
|
allStatus: '全部状态',
|
||||||
searchProxies: '搜索代理...',
|
searchProxies: '搜索代理...',
|
||||||
|
|||||||
@@ -124,7 +124,54 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #cell-address="{ row }">
|
<template #cell-address="{ row }">
|
||||||
<code class="code text-xs">{{ row.host }}:{{ row.port }}</code>
|
<div class="flex items-center gap-1.5">
|
||||||
|
<code class="code text-xs">{{ row.host }}:{{ row.port }}</code>
|
||||||
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded p-0.5 text-gray-400 hover:text-primary-600 dark:hover:text-primary-400"
|
||||||
|
:title="t('admin.proxies.copyProxyUrl')"
|
||||||
|
@click.stop="copyProxyUrl(row)"
|
||||||
|
@contextmenu.prevent="toggleCopyMenu(row.id)"
|
||||||
|
>
|
||||||
|
<Icon name="copy" size="sm" />
|
||||||
|
</button>
|
||||||
|
<!-- 右键展开格式选择菜单 -->
|
||||||
|
<div
|
||||||
|
v-if="copyMenuProxyId === row.id"
|
||||||
|
class="absolute left-0 top-full z-50 mt-1 w-auto min-w-[180px] rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-dark-500 dark:bg-dark-700"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="fmt in getCopyFormats(row)"
|
||||||
|
:key="fmt.label"
|
||||||
|
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs hover:bg-gray-100 dark:hover:bg-dark-600"
|
||||||
|
@click.stop="copyFormat(fmt.value)"
|
||||||
|
>
|
||||||
|
<span class="truncate font-mono text-gray-600 dark:text-gray-300">{{ fmt.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell-auth="{ row }">
|
||||||
|
<div v-if="row.username || row.password" class="flex items-center gap-1.5">
|
||||||
|
<div class="flex flex-col text-xs">
|
||||||
|
<span v-if="row.username" class="text-gray-700 dark:text-gray-200">{{ row.username }}</span>
|
||||||
|
<span v-if="row.password" class="font-mono text-gray-500 dark:text-gray-400">
|
||||||
|
{{ visiblePasswordIds.has(row.id) ? row.password : '••••••' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="row.password"
|
||||||
|
type="button"
|
||||||
|
class="ml-1 rounded p-0.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
@click.stop="visiblePasswordIds.has(row.id) ? visiblePasswordIds.delete(row.id) : visiblePasswordIds.add(row.id)"
|
||||||
|
>
|
||||||
|
<Icon :name="visiblePasswordIds.has(row.id) ? 'eyeOff' : 'eye'" size="sm" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-sm text-gray-400">-</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #cell-location="{ row }">
|
<template #cell-location="{ row }">
|
||||||
@@ -397,12 +444,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label">{{ t('admin.proxies.password') }}</label>
|
<label class="input-label">{{ t('admin.proxies.password') }}</label>
|
||||||
<input
|
<div class="relative">
|
||||||
v-model="createForm.password"
|
<input
|
||||||
type="password"
|
v-model="createForm.password"
|
||||||
class="input"
|
:type="createPasswordVisible ? 'text' : 'password'"
|
||||||
:placeholder="t('admin.proxies.optionalAuth')"
|
class="input pr-10"
|
||||||
/>
|
:placeholder="t('admin.proxies.optionalAuth')"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
@click="createPasswordVisible = !createPasswordVisible"
|
||||||
|
>
|
||||||
|
<Icon :name="createPasswordVisible ? 'eyeOff' : 'eye'" size="md" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
@@ -581,12 +637,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label">{{ t('admin.proxies.password') }}</label>
|
<label class="input-label">{{ t('admin.proxies.password') }}</label>
|
||||||
<input
|
<div class="relative">
|
||||||
v-model="editForm.password"
|
<input
|
||||||
type="password"
|
v-model="editForm.password"
|
||||||
:placeholder="t('admin.proxies.leaveEmptyToKeep')"
|
:type="editPasswordVisible ? 'text' : 'password'"
|
||||||
class="input"
|
:placeholder="t('admin.proxies.leaveEmptyToKeep')"
|
||||||
/>
|
class="input pr-10"
|
||||||
|
@input="editPasswordDirty = true"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
@click="editPasswordVisible = !editPasswordVisible"
|
||||||
|
>
|
||||||
|
<Icon :name="editPasswordVisible ? 'eyeOff' : 'eye'" size="md" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label">{{ t('admin.proxies.status') }}</label>
|
<label class="input-label">{{ t('admin.proxies.status') }}</label>
|
||||||
@@ -813,15 +879,18 @@ import ImportDataModal from '@/components/admin/proxy/ImportDataModal.vue'
|
|||||||
import Select from '@/components/common/Select.vue'
|
import Select from '@/components/common/Select.vue'
|
||||||
import Icon from '@/components/icons/Icon.vue'
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
||||||
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { copyToClipboard } = useClipboard()
|
||||||
|
|
||||||
const columns = computed<Column[]>(() => [
|
const columns = computed<Column[]>(() => [
|
||||||
{ key: 'select', label: '', sortable: false },
|
{ key: 'select', label: '', sortable: false },
|
||||||
{ key: 'name', label: t('admin.proxies.columns.name'), sortable: true },
|
{ key: 'name', label: t('admin.proxies.columns.name'), sortable: true },
|
||||||
{ key: 'protocol', label: t('admin.proxies.columns.protocol'), sortable: true },
|
{ key: 'protocol', label: t('admin.proxies.columns.protocol'), sortable: true },
|
||||||
{ key: 'address', label: t('admin.proxies.columns.address'), sortable: false },
|
{ key: 'address', label: t('admin.proxies.columns.address'), sortable: false },
|
||||||
|
{ key: 'auth', label: t('admin.proxies.columns.auth'), sortable: false },
|
||||||
{ key: 'location', label: t('admin.proxies.columns.location'), sortable: false },
|
{ key: 'location', label: t('admin.proxies.columns.location'), sortable: false },
|
||||||
{ key: 'account_count', label: t('admin.proxies.columns.accounts'), sortable: true },
|
{ key: 'account_count', label: t('admin.proxies.columns.accounts'), sortable: true },
|
||||||
{ key: 'latency', label: t('admin.proxies.columns.latency'), sortable: false },
|
{ key: 'latency', label: t('admin.proxies.columns.latency'), sortable: false },
|
||||||
@@ -858,6 +927,8 @@ const editStatusOptions = computed(() => [
|
|||||||
])
|
])
|
||||||
|
|
||||||
const proxies = ref<Proxy[]>([])
|
const proxies = ref<Proxy[]>([])
|
||||||
|
const visiblePasswordIds = reactive(new Set<number>())
|
||||||
|
const copyMenuProxyId = ref<number | null>(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
@@ -872,7 +943,10 @@ const pagination = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const showCreateModal = ref(false)
|
const showCreateModal = ref(false)
|
||||||
|
const createPasswordVisible = ref(false)
|
||||||
const showEditModal = ref(false)
|
const showEditModal = ref(false)
|
||||||
|
const editPasswordVisible = ref(false)
|
||||||
|
const editPasswordDirty = ref(false)
|
||||||
const showImportData = ref(false)
|
const showImportData = ref(false)
|
||||||
const showDeleteDialog = ref(false)
|
const showDeleteDialog = ref(false)
|
||||||
const showBatchDeleteDialog = ref(false)
|
const showBatchDeleteDialog = ref(false)
|
||||||
@@ -1030,6 +1104,7 @@ const closeCreateModal = () => {
|
|||||||
createForm.port = 8080
|
createForm.port = 8080
|
||||||
createForm.username = ''
|
createForm.username = ''
|
||||||
createForm.password = ''
|
createForm.password = ''
|
||||||
|
createPasswordVisible.value = false
|
||||||
batchInput.value = ''
|
batchInput.value = ''
|
||||||
batchParseResult.total = 0
|
batchParseResult.total = 0
|
||||||
batchParseResult.valid = 0
|
batchParseResult.valid = 0
|
||||||
@@ -1173,14 +1248,18 @@ const handleEdit = (proxy: Proxy) => {
|
|||||||
editForm.host = proxy.host
|
editForm.host = proxy.host
|
||||||
editForm.port = proxy.port
|
editForm.port = proxy.port
|
||||||
editForm.username = proxy.username || ''
|
editForm.username = proxy.username || ''
|
||||||
editForm.password = ''
|
editForm.password = proxy.password || ''
|
||||||
editForm.status = proxy.status
|
editForm.status = proxy.status
|
||||||
|
editPasswordVisible.value = false
|
||||||
|
editPasswordDirty.value = false
|
||||||
showEditModal.value = true
|
showEditModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeEditModal = () => {
|
const closeEditModal = () => {
|
||||||
showEditModal.value = false
|
showEditModal.value = false
|
||||||
editingProxy.value = null
|
editingProxy.value = null
|
||||||
|
editPasswordVisible.value = false
|
||||||
|
editPasswordDirty.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdateProxy = async () => {
|
const handleUpdateProxy = async () => {
|
||||||
@@ -1209,10 +1288,9 @@ const handleUpdateProxy = async () => {
|
|||||||
status: editForm.status
|
status: editForm.status
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only include password if it was changed
|
// Only include password if user actually modified the field
|
||||||
const trimmedPassword = editForm.password.trim()
|
if (editPasswordDirty.value) {
|
||||||
if (trimmedPassword) {
|
updateData.password = editForm.password.trim() || null
|
||||||
updateData.password = trimmedPassword
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await adminAPI.proxies.update(editingProxy.value.id, updateData)
|
await adminAPI.proxies.update(editingProxy.value.id, updateData)
|
||||||
@@ -1715,12 +1793,60 @@ const closeAccountsModal = () => {
|
|||||||
proxyAccounts.value = []
|
proxyAccounts.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Proxy URL copy ──
|
||||||
|
function buildAuthPart(row: any): string {
|
||||||
|
const user = row.username ? encodeURIComponent(row.username) : ''
|
||||||
|
const pass = row.password ? encodeURIComponent(row.password) : ''
|
||||||
|
if (user && pass) return `${user}:${pass}@`
|
||||||
|
if (user) return `${user}@`
|
||||||
|
if (pass) return `:${pass}@`
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProxyUrl(row: any): string {
|
||||||
|
return `${row.protocol}://${buildAuthPart(row)}${row.host}:${row.port}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCopyFormats(row: any) {
|
||||||
|
const hasAuth = row.username || row.password
|
||||||
|
const fullUrl = buildProxyUrl(row)
|
||||||
|
const formats = [
|
||||||
|
{ label: fullUrl, value: fullUrl },
|
||||||
|
]
|
||||||
|
if (hasAuth) {
|
||||||
|
const withoutProtocol = fullUrl.replace(/^[^:]+:\/\//, '')
|
||||||
|
formats.push({ label: withoutProtocol, value: withoutProtocol })
|
||||||
|
}
|
||||||
|
formats.push({ label: `${row.host}:${row.port}`, value: `${row.host}:${row.port}` })
|
||||||
|
return formats
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyProxyUrl(row: any) {
|
||||||
|
copyToClipboard(buildProxyUrl(row), t('admin.proxies.urlCopied'))
|
||||||
|
copyMenuProxyId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCopyMenu(id: number) {
|
||||||
|
copyMenuProxyId.value = copyMenuProxyId.value === id ? null : id
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyFormat(value: string) {
|
||||||
|
copyToClipboard(value, t('admin.proxies.urlCopied'))
|
||||||
|
copyMenuProxyId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCopyMenu() {
|
||||||
|
copyMenuProxyId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadProxies()
|
loadProxies()
|
||||||
|
document.addEventListener('click', closeCopyMenu)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearTimeout(searchTimeout)
|
clearTimeout(searchTimeout)
|
||||||
abortController?.abort()
|
abortController?.abort()
|
||||||
|
document.removeEventListener('click', closeCopyMenu)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user