feat:历史记录分享功能实现

This commit is contained in:
liailing1026
2026-03-12 13:35:04 +08:00
parent 26c42697e8
commit 130f78108f
9 changed files with 1500 additions and 80 deletions

View File

@@ -0,0 +1,471 @@
<template>
<el-dialog
v-model="dialogVisible"
:width="width"
:close-on-click-modal="true"
:center="true"
:show-close="true"
top="20vh"
title="链接分享"
@closed="handleClosed"
>
<div class="share-content">
<!-- 设置区域 -->
<div v-if="!loading && !error" class="settings-section">
<!-- 有效期设置 -->
<div class="setting-row inline">
<div class="setting-label">有效期</div>
<div class="option-box">
<div
v-for="option in expirationOptions"
:key="option.value"
:class="['option-item', { active: expirationDays === option.value }]"
@click="expirationDays = option.value"
>
{{ option.label }}
</div>
</div>
</div>
<!-- 提取码设置 -->
<div class="setting-row inline">
<div class="setting-label">提取码</div>
<div class="code-option-box">
<div
:class="['option-item', { active: codeType === 'random' }]"
@click="handleCodeTypeChange('random')"
>
随机生成
</div>
<div
:class="['option-item', { active: codeType === 'custom' }]"
@click="handleCodeTypeChange('custom')"
>
自定义
</div>
</div>
<!-- 自定义输入框 -->
<div v-if="codeType === 'custom'" class="custom-code-wrapper">
<el-input
v-model="customCode"
placeholder="仅支持字母/数字"
class="custom-code-input"
maxlength="4"
@input="validateCustomCode"
>
<template #suffix>
<span v-if="customCode" class="code-count">{{ customCode.length }}/4</span>
</template>
</el-input>
<span v-if="!customCode" class="code-hint">请输入提取码</span>
</div>
</div>
<!-- 自动填充提取码选项 -->
<div class="setting-row">
<el-checkbox v-model="autoFillCode" class="auto-fill-checkbox">
分享链接自动填充提取码
</el-checkbox>
</div>
<!-- 复制链接按钮 -->
<el-button
type="primary"
size="large"
class="generate-btn"
:disabled="!canGenerate"
:loading="loading"
@click="handleCopyLink"
>
复制链接
</el-button>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-state">
<div class="error-icon">
<SvgIcon icon-class="JingGao" size="48px" color="#f56c6c" />
</div>
<p class="error-text">{{ error }}</p>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import SvgIcon from '@/components/SvgIcon/index.vue'
import websocket from '@/utils/websocket'
interface Props {
modelValue: boolean
planId?: string
width?: string | number
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
planId: '',
width: '450px'
})
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
// 有效期选项
const expirationOptions = [
{ label: '1天', value: 1 },
{ label: '7天', value: 7 },
{ label: '30天', value: 30 },
{ label: '365天', value: 365 },
{ label: '永久有效', value: 0 }
]
const loading = ref(false)
const error = ref('')
const dialogVisible = ref(props.modelValue)
const expirationDays = ref(7) // 默认7天
const codeType = ref<'random' | 'custom'>('random') // 提取码类型
const customCode = ref('') // 自定义提取码
const generatedCode = ref('') // 生成的提取码
const autoFillCode = ref(true) // 分享链接自动填充提取码
// 是否可以生成链接
const canGenerate = computed(() => {
if (codeType.value === 'random') {
return true // 随机生成总是可以
} else {
return /^[a-zA-Z0-9]{4}$/.test(customCode.value)
}
})
// 生成唯一请求ID
let requestIdCounter = 0
const generateRequestId = () => `ws_req_${Date.now()}_${++requestIdCounter}`
// 监听 v-model 变化
watch(
() => props.modelValue,
val => {
dialogVisible.value = val
if (val && props.planId) {
// 打开弹窗时重置状态
loading.value = false
error.value = ''
// 生成随机提取码
generateRandomCode()
}
}
)
// 监听对话框显示状态变化
watch(dialogVisible, val => {
emit('update:modelValue', val)
})
// 生成随机提取码4位字母数字
const generateRandomCode = () => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
let code = ''
for (let i = 0; i < 4; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length))
}
generatedCode.value = code
}
// 处理提取码类型变化
const handleCodeTypeChange = (type: 'random' | 'custom') => {
codeType.value = type
if (type === 'random') {
generateRandomCode()
}
}
// 验证自定义提取码
const validateCustomCode = (value: string) => {
// 只允许字母和数字
customCode.value = value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()
}
// 生成并复制链接
const handleCopyLink = async () => {
if (!props.planId) {
error.value = '任务 ID 为空'
return
}
// 获取提取码
const extractionCode = codeType.value === 'random' ? generatedCode.value : customCode.value
loading.value = true
error.value = ''
const reqId = generateRequestId()
try {
const result = await websocket.send('share_plan', {
id: reqId,
data: {
plan_id: props.planId,
expiration_days: expirationDays.value,
extraction_code: extractionCode,
auto_fill_code: autoFillCode.value
}
})
if (result.data?.share_url) {
// 获取当前域名
const baseUrl = window.location.origin
const fullUrl = baseUrl + result.data.share_url
// 复制到剪贴板
try {
await navigator.clipboard.writeText(fullUrl)
ElMessage.success('链接已复制到剪贴板')
dialogVisible.value = false
} catch {
ElMessage.error('复制失败,请手动复制')
}
} else {
error.value = '生成分享链接失败'
}
} catch (err: any) {
error.value = err.message || '生成分享链接失败'
} finally {
loading.value = false
}
}
// 对话框关闭后重置状态
const handleClosed = () => {
loading.value = false
error.value = ''
expirationDays.value = 7
codeType.value = 'random'
customCode.value = ''
generatedCode.value = ''
autoFillCode.value = true
}
// 暴露方法供外部调用
defineExpose({
close: () => {
dialogVisible.value = false
}
})
</script>
<style scoped lang="scss">
.share-content {
padding: 20px 0;
}
.settings-section {
.setting-row {
margin-bottom: 20px;
&.inline {
display: flex;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
.setting-label {
margin-bottom: 0;
margin-right: 12px;
white-space: nowrap;
}
.option-box {
flex: 1;
min-width: 200px;
}
.code-option-box {
flex: none;
}
.custom-code-wrapper {
margin-left: 8px;
}
}
}
.setting-label {
font-size: 14px;
color: var(--color-text-primary);
margin-bottom: 8px;
font-weight: 500;
}
.option-box {
display: flex;
border: 1px solid var(--color-border);
border-radius: 6px;
overflow: hidden;
.option-item {
flex: 1;
padding: 10px 0;
text-align: center;
cursor: pointer;
font-size: 13px;
color: var(--color-text-secondary);
border-right: 1px solid var(--color-border);
transition: all 0.2s;
&:last-child {
border-right: none;
}
&:hover {
background-color: var(--color-bg-subtle);
}
&.active {
background-color: var(--color-primary);
color: white;
}
}
}
.code-option-box {
display: flex;
width: 160px;
border: 1px solid var(--color-border);
border-radius: 6px;
overflow: hidden;
.option-item {
flex: 1;
padding: 10px 0;
text-align: center;
cursor: pointer;
font-size: 13px;
color: var(--color-text-secondary);
border-right: 1px solid var(--color-border);
transition: all 0.2s;
&:last-child {
border-right: none;
}
&:hover {
background-color: var(--color-bg-subtle);
}
&.active {
background-color: var(--color-primary);
color: white;
}
}
}
.custom-code-wrapper {
display: flex;
align-items: center;
margin-left: 8px;
.custom-code-input {
width: 100px;
:deep(.el-input__wrapper) {
padding-right: 8px;
}
:deep(.el-input__suffix) {
right: 4px;
}
:deep(.el-input__inner) {
&::placeholder {
font-size: 11px;
color: #999;
}
}
.code-count {
font-size: 10px;
color: var(--color-text-secondary);
}
}
.code-hint {
font-size: 12px;
color: var(--color-error, #f56c6c);
margin-left: 8px;
white-space: nowrap;
}
}
.auto-fill-checkbox {
margin-top: 8px;
}
.generate-btn {
margin-top: 24px;
width: 100%;
}
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
gap: 12px;
color: var(--color-text-secondary);
.el-icon {
font-size: 32px;
}
}
.success-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
.success-icon {
margin-bottom: 16px;
}
.success-text {
font-size: 16px;
font-weight: 500;
color: var(--color-text-primary);
margin-bottom: 20px;
}
.share-link-box {
width: 100%;
margin-bottom: 16px;
.link-input {
width: 100%;
}
}
.tip-text {
font-size: 13px;
color: var(--color-text-secondary);
text-align: center;
}
}
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
gap: 12px;
.error-text {
color: var(--color-error, #f56c6c);
font-size: 14px;
}
}
</style>