优化: miniapp 设置页支持编辑 + 移除 Ideas 批量删除功能
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { getConfig } from '@/config'
|
||||
import { computed, ref } from 'vue'
|
||||
import { getConfig, setConfig } from '@/config'
|
||||
import { useAiChat } from '@/composables/useAiChat'
|
||||
|
||||
/**
|
||||
* 设置页(只读查看,不编辑)。
|
||||
* 设置页(连接配置查看/编辑 + 连接状态 + 连接操作)。
|
||||
*
|
||||
* 展示当前连接配置 + 连接状态,用户据此核对/排查问题。
|
||||
* 编辑功能后续做(relayHost/deviceId/token 手填)或走二维码配对(分发阶段)。
|
||||
* 默认查看模式(只读展示 relayHost/deviceId/token,token 脱敏);点「编辑」进入编辑模式,
|
||||
* 保存时 setConfig 持久化并 resumeIfDisconnected 用新配置重连。连接态可「断开」,
|
||||
* 断开态可「重连」,另展示最近状态详情(wsStatusDetail)辅助排查。
|
||||
*
|
||||
* 数据源:
|
||||
* - 连接配置:config.ts getConfig()(storage 持久化,含默认值合并)
|
||||
@@ -21,6 +22,7 @@ const cfg = computed(() => getConfig())
|
||||
// 连接状态(实时,useAiChat 单例 ref 直接响应)
|
||||
const ai = useAiChat()
|
||||
const wsStatus = computed(() => ai.wsStatus.value)
|
||||
const wsStatusDetail = computed(() => ai.wsStatusDetail.value)
|
||||
const deviceOnline = computed(() => ai.deviceOnline.value)
|
||||
const isWsConnected = computed(() => ai.isWsConnected.value)
|
||||
|
||||
@@ -58,11 +60,56 @@ function fmtMs(ms: number): string {
|
||||
/** 版本号(对齐 manifest.json versionName,改版时同步) */
|
||||
const APP_VERSION = '0.1.0'
|
||||
|
||||
/** 手动重连(断开/重连中可点,复用 ai.resumeIfDisconnected) */
|
||||
/** 手动重连(断开态可点)。用 ai.connect() 而非 resumeIfDisconnected:
|
||||
* 用户先点「断开」(manualClose=true)后 resumeIfDisconnected 会因 manualClose 早退,
|
||||
* connect() 复位 manualClose 强制开新连接(非连接态才生效,连接态自动幂等跳过)。 */
|
||||
function onReconnect(): void {
|
||||
if (wsStatus.value === 'disconnected' || wsStatus.value === 'reconnecting') {
|
||||
ai.resumeIfDisconnected()
|
||||
ai.connect()
|
||||
}
|
||||
|
||||
/** 手动断开(连接态可点,disconnect 置 manualClose 后不再自动重连) */
|
||||
function onDisconnect(): void {
|
||||
ai.disconnect()
|
||||
uni.showToast({ title: '已断开连接', icon: 'none' })
|
||||
}
|
||||
|
||||
/** 连接操作按钮入口(连接态→断开,断开态→重连) */
|
||||
function onConnOp(): void {
|
||||
if (isWsConnected.value) onDisconnect()
|
||||
else onReconnect()
|
||||
}
|
||||
|
||||
/** 编辑态(false=查看模式,true=编辑模式;草稿保存时才 setConfig,取消丢弃) */
|
||||
const editing = ref(false)
|
||||
const draftRelayHost = ref('')
|
||||
const draftDeviceId = ref('')
|
||||
const draftToken = ref('')
|
||||
|
||||
/** 进入编辑模式:以当前配置填充草稿 */
|
||||
function enterEdit(): void {
|
||||
draftRelayHost.value = cfg.value.relayHost
|
||||
draftDeviceId.value = cfg.value.deviceId
|
||||
draftToken.value = cfg.value.token
|
||||
editing.value = true
|
||||
}
|
||||
|
||||
/** 取消编辑:丢弃草稿回查看模式(不保存) */
|
||||
function cancelEdit(): void {
|
||||
editing.value = false
|
||||
}
|
||||
|
||||
/** 保存编辑:setConfig 持久化 + 提示 + resumeIfDisconnected 用新配置重连 */
|
||||
function saveEdit(): void {
|
||||
const relayHost = draftRelayHost.value.trim()
|
||||
const deviceId = draftDeviceId.value.trim()
|
||||
if (!relayHost || !deviceId) {
|
||||
uni.showToast({ title: '中继地址和设备 ID 不能为空', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setConfig({ relayHost, deviceId, token: draftToken.value.trim() })
|
||||
editing.value = false
|
||||
uni.showToast({ title: '已保存,正在重连…', icon: 'none' })
|
||||
ai.resumeIfDisconnected()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -81,21 +128,41 @@ function onReconnect(): void {
|
||||
<text class="row-label">中继地址</text>
|
||||
<text class="row-value mono">{{ cfg.relayHost }}</text>
|
||||
</view>
|
||||
<view class="row" v-if="wsStatusDetail">
|
||||
<text class="row-label">最近状态</text>
|
||||
<text class="row-value">{{ wsStatusDetail }}</text>
|
||||
</view>
|
||||
<view class="ops">
|
||||
<view class="btn btn-block" :class="isWsConnected ? 'btn-danger' : 'btn-primary'" @tap="onConnOp">
|
||||
{{ isWsConnected ? '断开' : '重连' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 连接配置(只读展示,编辑后续做) -->
|
||||
<!-- 连接配置(查看/编辑双模式:编辑可改 relayHost/deviceId/token) -->
|
||||
<view class="card">
|
||||
<view class="card-head">
|
||||
<text class="card-title">连接配置</text>
|
||||
<text class="card-sub">只读 · 编辑后续开放</text>
|
||||
<view v-if="editing" class="card-actions">
|
||||
<text class="btn btn-sm btn-ghost" @tap="cancelEdit">取消</text>
|
||||
<text class="btn btn-sm btn-primary" @tap="saveEdit">保存</text>
|
||||
</view>
|
||||
<text v-else class="btn btn-sm btn-ghost" @tap="enterEdit">编辑</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="row-label">中继地址</text>
|
||||
<input v-if="editing" v-model="draftRelayHost" class="row-input" placeholder="ws://…/ws/miniapp" placeholder-class="row-ph" />
|
||||
<text v-else class="row-value mono">{{ cfg.relayHost }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="row-label">设备 ID</text>
|
||||
<text class="row-value mono">{{ cfg.deviceId }}</text>
|
||||
<input v-if="editing" v-model="draftDeviceId" class="row-input" placeholder="桌面端设备 ID" placeholder-class="row-ph" />
|
||||
<text v-else class="row-value mono">{{ cfg.deviceId }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="row-label">配对 Token</text>
|
||||
<text class="row-value mono">{{ maskToken(cfg.token) }}</text>
|
||||
<input v-if="editing" v-model="draftToken" class="row-input" placeholder="配对 token" placeholder-class="row-ph" />
|
||||
<text v-else class="row-value mono">{{ maskToken(cfg.token) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -197,4 +264,59 @@ function onReconnect(): void {
|
||||
.st-wait { color: $uni-color-primary; }
|
||||
.st-warn { color: #f0c75e; }
|
||||
.st-err { color: #e85a4f; }
|
||||
|
||||
/* 连接操作按钮(连接状态卡:重连/断开) */
|
||||
.ops {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8rpx 24rpx;
|
||||
font-size: $uni-font-size-sm;
|
||||
line-height: 1.6;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
.btn-block {
|
||||
display: block;
|
||||
padding: 20rpx 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: $uni-color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-danger {
|
||||
background-color: #e85a4f;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-ghost {
|
||||
color: $uni-color-primary;
|
||||
border: 1rpx solid $uni-color-primary;
|
||||
background-color: transparent;
|
||||
}
|
||||
/* 卡片头部按钮组(编辑/取消/保存) */
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.card-actions .btn {
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
.btn-sm {
|
||||
padding: 4rpx 20rpx;
|
||||
}
|
||||
/* 编辑态输入(label 左 + input 右,横向一行对齐 .row-value) */
|
||||
.row-input {
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
font-size: $uni-font-size-sm;
|
||||
color: $uni-text-color;
|
||||
text-align: right;
|
||||
}
|
||||
.row-ph {
|
||||
color: $uni-text-color-disable;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -141,10 +141,5 @@ export default {
|
||||
confirmStatus: 'Change idea "{title}" status from {from} to {to}?',
|
||||
promoteFailed: 'Promotion failed',
|
||||
evalFailed: 'Evaluation failed',
|
||||
// Batch operations
|
||||
batchDelete: 'Batch delete',
|
||||
confirmBatchDelete: 'Delete {n} selected ideas? This cannot be undone.',
|
||||
batchDeleteSuccess: 'Deleted {n} idea(s)',
|
||||
selectedCount: '{n} selected',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -141,10 +141,5 @@ export default {
|
||||
confirmStatus: '将灵感「{title}」状态从 {from} 改为 {to}?',
|
||||
promoteFailed: '立项失败',
|
||||
evalFailed: '评估失败',
|
||||
// 批量操作
|
||||
batchDelete: '批量删除',
|
||||
confirmBatchDelete: '确定删除选中的 {n} 个灵感?此操作不可撤销。',
|
||||
batchDeleteSuccess: '已删除 {n} 个灵感',
|
||||
selectedCount: '已选 {n} 项',
|
||||
},
|
||||
}
|
||||
|
||||
+1
-97
@@ -45,27 +45,6 @@
|
||||
<div class="ideas-layout">
|
||||
<!-- 左侧:灵感列表 -->
|
||||
<section class="idea-list-panel">
|
||||
<div class="idea-list-header">
|
||||
<label class="idea-batch-check" v-if="filteredIdeas.length > 0">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedAll"
|
||||
:indeterminate="selectedAny && !selectedAll"
|
||||
@change="toggleSelectAll"
|
||||
/>
|
||||
<span v-if="selectedAny" class="batch-selected-note">{{ $t('ideas.selectedCount', { n: selectedIds.size }) }}</span>
|
||||
</label>
|
||||
<div class="idea-list-actions">
|
||||
<button
|
||||
v-if="selectedAny"
|
||||
class="btn btn-danger btn-sm"
|
||||
:disabled="batchDeleting"
|
||||
@click="confirmBatchDelete"
|
||||
>
|
||||
{{ batchDeleting ? $t('ideas.deleting') : $t('ideas.batchDelete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="idea-list">
|
||||
<!-- 三态:加载/错误/空(对齐 Tasks.vue:43-45) -->
|
||||
<div v-if="store.loading" class="empty-state">{{ $t('common.loading') }}</div>
|
||||
@@ -79,14 +58,6 @@
|
||||
:class="{ selected: selectedId === idea.id }"
|
||||
@click="selectedId = idea.id"
|
||||
>
|
||||
<div class="idea-card-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedIds.has(idea.id)"
|
||||
@click.stop
|
||||
@change="toggleSelect(idea.id)"
|
||||
/>
|
||||
</div>
|
||||
<div class="idea-card-body">
|
||||
<div class="idea-card-header">
|
||||
<span class="idea-title">{{ idea.title }}</span>
|
||||
@@ -180,7 +151,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, type Ref } from 'vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
@@ -211,45 +182,6 @@ type FilterKey = 'all' | 'hot' | 'pending' | 'promoted'
|
||||
const activeFilter = usePersistedRef<FilterKey>('ideas.activeFilter', 'all')
|
||||
const selectedId = ref<string | null>(null)
|
||||
const searchQuery = ref('')
|
||||
// 批量选择
|
||||
const selectedIds: Ref<Set<string>> = ref(new Set()) as Ref<Set<string>>
|
||||
const batchDeleting = ref(false)
|
||||
const selectedAll = computed(
|
||||
() => filteredIdeas.value.length > 0 && selectedIds.value.size === filteredIdeas.value.length,
|
||||
)
|
||||
const selectedAny = computed(() => selectedIds.value.size > 0)
|
||||
function toggleSelect(id: string) {
|
||||
const next = new Set(selectedIds.value)
|
||||
if (next.has(id)) next.delete(id); else next.add(id)
|
||||
selectedIds.value = next
|
||||
}
|
||||
function toggleSelectAll() {
|
||||
if (selectedAll.value) {
|
||||
selectedIds.value = new Set()
|
||||
} else {
|
||||
selectedIds.value = new Set(filteredIdeas.value.map(i => i.id))
|
||||
}
|
||||
}
|
||||
async function confirmBatchDelete() {
|
||||
if (selectedIds.value.size === 0) return
|
||||
if (!await confirmDialog(t('ideas.confirmBatchDelete', { n: selectedIds.value.size }), t('common.delete'))) return
|
||||
batchDeleting.value = true
|
||||
const ids = [...selectedIds.value]
|
||||
try {
|
||||
for (const id of ids) {
|
||||
await store.deleteIdea(id)
|
||||
}
|
||||
selectedIds.value = new Set()
|
||||
selectedId.value = null
|
||||
Message.success(t('ideas.batchDeleteSuccess', { n: ids.length }))
|
||||
await store.loadIdeas(buildIdeaQuery())
|
||||
} catch (e) {
|
||||
Message.error(t('ideas.deleteFailed'))
|
||||
console.error('[Ideas] 批量删除失败:', e)
|
||||
} finally {
|
||||
batchDeleting.value = false
|
||||
}
|
||||
}
|
||||
// 列表排序:time 新在前 / score 高分在前(默认 time,按创建时间倒序)
|
||||
const sortMode = usePersistedRef<'score' | 'time'>('ideas.sortMode', 'time')
|
||||
|
||||
@@ -575,7 +507,6 @@ watch(() => route.params.id, (id) => {
|
||||
.idea-card-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
/* 批量操作重构后 desc/footer 移入 body,须纵向堆叠(原 .idea-card 直接子元素横排 bug) */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -586,12 +517,6 @@ watch(() => route.params.id, (id) => {
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.idea-card-check {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.idea-card-check input { cursor: pointer; }
|
||||
.idea-card:hover { background: var(--df-accent-bg); }
|
||||
.idea-card.selected { background: var(--df-accent-soft); border: 0.5px solid var(--df-accent); }
|
||||
|
||||
@@ -622,27 +547,6 @@ watch(() => route.params.id, (id) => {
|
||||
}
|
||||
.idea-date { font-size: 11px; color: var(--df-text-dim); }
|
||||
|
||||
/* ===== 批量操作头 ===== */
|
||||
.idea-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 14px 8px;
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.idea-batch-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--df-text-dim);
|
||||
cursor: pointer;
|
||||
}
|
||||
.idea-batch-check input { cursor: pointer; }
|
||||
.batch-selected-note { font-size: 11px; color: var(--df-text-dim); white-space: nowrap; }
|
||||
.idea-list-actions { margin-left: auto; }
|
||||
|
||||
/* ===== 状态标签已提取到 styles/components.css 全局(.status-tag 系列) ===== */
|
||||
|
||||
/* ===== 右侧详情(详情内容由 IdeaDetail 子组件渲染,父级仅保留未选中空态壳) ===== */
|
||||
|
||||
Reference in New Issue
Block a user