feat: Refactor channel management and UI components
- Removed hardcoded channel data from `channel.ts` and replaced it with a dynamic channel dictionary. - Introduced a new Pinia store `channel.ts` to manage selected and available channels. - Reworked `AddChannelDialog.vue` to allow users to search and select channels dynamically. - Updated `TaskCenter.vue` to utilize the new channel store and handle empty channel selections gracefully. - Enhanced IPC communication for loading and saving selected channels in the configuration. - Adjusted `runTaskOperationService.ts` to ensure proper handling of channel data. - Improved styling and structure of UI components for better user experience.
This commit is contained in:
@@ -1,25 +1,41 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export interface Item {
|
||||
id: string
|
||||
channelName: string
|
||||
channelUrl: string
|
||||
}
|
||||
|
||||
export const channels: Item[] = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
channelName: 'fliggy',
|
||||
channelUrl: 'https://hotel.fliggy.com/ebooking/hotelBaseInfoUv.htm#/ebk/homeV1',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
channelName: 'meituan',
|
||||
channelUrl: 'https://me.meituan.com/ebooking/merchant/product#/index',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
channelName: 'douyin',
|
||||
channelUrl: 'https://life.douyin.com/p/travel-ari/hotel/price_amount_state?groupid=1816249020842116',
|
||||
export const channelDictionary: Record<string, string> = {
|
||||
fliggy: 'https://hotel.fliggy.com/ebooking/hotelBaseInfoUv.htm#/ebk/homeV1',
|
||||
meituan: 'https://me.meituan.com/ebooking/merchant/product#/index',
|
||||
douyin: 'https://life.douyin.com/p/travel-ari/hotel/price_amount_state?groupid=1816249020842116',
|
||||
}
|
||||
|
||||
export function resolveChannel(value: string): { name?: string; url: string } {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) {
|
||||
return { url: '' }
|
||||
}
|
||||
]
|
||||
|
||||
// 如果是已知渠道名称
|
||||
if (channelDictionary[trimmed]) {
|
||||
return { name: trimmed, url: channelDictionary[trimmed] }
|
||||
}
|
||||
|
||||
// 如果是 URL
|
||||
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
||||
// 尝试反向查找名称
|
||||
const entry = Object.entries(channelDictionary).find(([, url]) => url === trimmed)
|
||||
if (entry) {
|
||||
return { name: entry[0], url: trimmed }
|
||||
}
|
||||
try {
|
||||
const hostname = new URL(trimmed).hostname
|
||||
return { name: hostname, url: trimmed }
|
||||
} catch {
|
||||
return { name: trimmed, url: trimmed }
|
||||
}
|
||||
}
|
||||
|
||||
// 其他情况:当作未知名称,URL 也用它本身(下游需自行判断)
|
||||
return { name: trimmed, url: trimmed }
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ export enum CONFIG_KEYS {
|
||||
DEFAULT_MODEL = 'defaultModel',
|
||||
AUTO_CHECK_UPDATE = 'autoCheckUpdate',
|
||||
AUTO_DOWNLOAD_UPDATE = 'autoDownloadUpdate',
|
||||
SELECTED_CHANNELS = 'selectedChannels',
|
||||
}
|
||||
|
||||
export enum MENU_IDS {
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface IConfig {
|
||||
[CONFIG_KEYS.PROVIDER]?: string;
|
||||
// 默认模型
|
||||
[CONFIG_KEYS.DEFAULT_MODEL]?: string | null;
|
||||
// 选中的渠道
|
||||
[CONFIG_KEYS.SELECTED_CHANNELS]: Array<{ id: string; channelName: string; channelUrl: string }>;
|
||||
}
|
||||
|
||||
export interface Provider {
|
||||
|
||||
@@ -34,18 +34,24 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Settings } from '@lucide/vue'
|
||||
import { taskCenterList, taskCenterItem } from '@constant/taskCenterList'
|
||||
import { channels } from '@constant/channel'
|
||||
import { useChannelStore } from '@stores/channel'
|
||||
import emitter from '@utils/emitter'
|
||||
|
||||
const channelStore = useChannelStore()
|
||||
const taskList = computed(() => taskCenterList)
|
||||
|
||||
// 点击任务项
|
||||
const handleTaskItem = (item: taskCenterItem) => {
|
||||
// 一键打开各渠道
|
||||
if (item.type === 'channel') {
|
||||
window.api.openChannel(channels.value)
|
||||
if (!channelStore.selectedChannels || channelStore.selectedChannels.length === 0) {
|
||||
ElMessage.warning('请先配置渠道')
|
||||
return
|
||||
}
|
||||
window.api.openChannel(JSON.parse(JSON.stringify(channelStore.selectedChannels)))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +1,120 @@
|
||||
<template>
|
||||
<el-dialog v-model="isVisible" width="480" align-center class="dark-dialog">
|
||||
<template #title>
|
||||
<span>添加渠道</span>
|
||||
</template>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-position="top" class="pl-4 pr-4 pt-4 dark-form">
|
||||
<el-form-item prop="channelName">
|
||||
<template #label>
|
||||
<span>渠道名称</span>
|
||||
</template>
|
||||
<el-input v-model="form.channelName" placeholder="请输入渠道名称" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="channelUrl">
|
||||
<template #label>
|
||||
<span>渠道链接</span>
|
||||
</template>
|
||||
<el-input v-model="form.channelUrl" placeholder="请输入渠道链接" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button class="cancel-btn" @click="cancel">取消</el-button>
|
||||
<el-button type="primary" @click="confirm">确认</el-button>
|
||||
<el-dialog
|
||||
v-model="isVisible"
|
||||
width="560"
|
||||
align-center
|
||||
class="custom-script-dialog"
|
||||
:show-close="false"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<template #header>
|
||||
<div class="sticky top-0 z-10 bg-[#F4F3EB] dark:bg-[#1f1f22] flex justify-between items-start">
|
||||
<div>
|
||||
<h2 class="text-[20px] font-serif text-[#171717] dark:text-[#f3f4f6] font-normal tracking-tight">
|
||||
关联渠道
|
||||
</h2>
|
||||
</div>
|
||||
<button @click="close" class="text-[#99A0AE] dark:text-gray-500 hover:text-[#171717] dark:hover:text-[#f3f4f6] transition-colors mt-[4px]">
|
||||
<el-icon class="text-[20px] cursor-pointer"><Close /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="px-[24px] pb-[24px] pt-[8px] space-y-5">
|
||||
<!-- Search -->
|
||||
<div class="space-y-2">
|
||||
<div class="text-[14px] text-[#171717]/80 dark:text-[#f3f4f6]/80 font-bold mb-2">搜索添加渠道</div>
|
||||
<el-autocomplete
|
||||
v-if="channelStore.availableChannels.length"
|
||||
v-model="searchQuery"
|
||||
:fetch-suggestions="querySearch"
|
||||
clearable
|
||||
placeholder="输入渠道名称或链接"
|
||||
class="w-full"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div class="flex flex-col py-1">
|
||||
<span class="text-[13px] font-medium text-[#171717] dark:text-[#f3f4f6]">{{ item.channelName }}</span>
|
||||
<span class="text-[12px] text-[#99A0AE] dark:text-gray-500 truncate">{{ item.channelUrl }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
<div v-else class="text-[13px] text-[#99A0AE] dark:text-gray-500 py-2">
|
||||
暂无可用渠道
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected list -->
|
||||
<div class="space-y-2">
|
||||
<div class="text-[14px] text-[#171717]/80 dark:text-[#f3f4f6]/80 font-bold mb-2">已选渠道</div>
|
||||
<div
|
||||
v-if="localSelected.length > 0"
|
||||
class="space-y-2 max-h-[240px] overflow-y-auto pr-1"
|
||||
>
|
||||
<div
|
||||
v-for="item in localSelected"
|
||||
:key="item.id"
|
||||
class="flex items-center justify-between gap-3 bg-[#E8E6DE]/50 dark:bg-[#222225] p-3 rounded-xl border border-black/5 dark:border-[#2a2a2d]"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-[13px] font-medium text-[#171717] dark:text-[#f3f4f6] truncate">
|
||||
{{ item.channelName }}
|
||||
</div>
|
||||
<el-tooltip :content="item.channelUrl" placement="top" :show-after="300">
|
||||
<div class="text-[12px] text-[#99A0AE] dark:text-gray-500 truncate cursor-default">
|
||||
{{ item.channelUrl }}
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<button
|
||||
class="shrink-0 text-[#99A0AE] dark:text-gray-500 hover:text-red-500 dark:hover:text-red-400 transition-colors"
|
||||
@click="removeItem(item.id)"
|
||||
>
|
||||
<el-icon class="text-[18px]"><Delete /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-[13px] text-[#99A0AE] dark:text-gray-500 py-4 text-center bg-[#E8E6DE]/30 dark:bg-[#222225]/50 rounded-xl border border-dashed border-black/5 dark:border-[#2a2a2d]">
|
||||
未选择任何渠道
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<el-button
|
||||
@click="cancel"
|
||||
class="!rounded-full !px-6 !h-[40px] !text-[13px] !font-semibold cancel-btn"
|
||||
>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="confirm"
|
||||
class="!rounded-full !px-6 !h-[40px] !text-[13px] !font-semibold"
|
||||
>
|
||||
确认
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Close, Delete } from '@element-plus/icons-vue'
|
||||
import { useChannelStore, type ChannelItem } from '@stores/channel'
|
||||
|
||||
const channelStore = useChannelStore()
|
||||
|
||||
const isVisible = ref(false)
|
||||
const formRef = ref()
|
||||
const form = ref({
|
||||
channelName: '',
|
||||
channelUrl: ''
|
||||
})
|
||||
const rules = ref({
|
||||
channelName: [
|
||||
{ required: true, message: '请输入渠道名称', trigger: 'blur' },
|
||||
],
|
||||
channelUrl: [
|
||||
{ required: true, message: '请输入渠道链接', trigger: 'blur' },
|
||||
]
|
||||
})
|
||||
const searchQuery = ref('')
|
||||
const localSelected = ref<ChannelItem[]>([])
|
||||
|
||||
const open = () => {
|
||||
const open = async () => {
|
||||
await channelStore.loadSelectedChannels()
|
||||
localSelected.value = channelStore.selectedChannels.map((item) => ({ ...item }))
|
||||
searchQuery.value = ''
|
||||
isVisible.value = true
|
||||
}
|
||||
|
||||
@@ -53,24 +123,46 @@ const close = () => {
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
form.value.channelName = ''
|
||||
form.value.channelUrl = ''
|
||||
formRef.value?.resetFields()
|
||||
searchQuery.value = ''
|
||||
localSelected.value = []
|
||||
}
|
||||
|
||||
const handleClosed = () => {
|
||||
reset()
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
close()
|
||||
reset()
|
||||
}
|
||||
|
||||
const confirm = () => {
|
||||
formRef.value.validate((valid: boolean) => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
close()
|
||||
reset()
|
||||
})
|
||||
const confirm = async () => {
|
||||
channelStore.setSelectedChannels(localSelected.value.map((item) => ({ ...item })))
|
||||
await channelStore.saveSelectedChannels()
|
||||
close()
|
||||
}
|
||||
|
||||
const querySearch = (queryString: string, cb: (results: ChannelItem[]) => void) => {
|
||||
const list = channelStore.availableChannels
|
||||
const query = queryString.trim().toLowerCase()
|
||||
const results = query
|
||||
? list.filter(
|
||||
(item) =>
|
||||
item.channelName.toLowerCase().includes(query) ||
|
||||
item.channelUrl.toLowerCase().includes(query)
|
||||
)
|
||||
: list
|
||||
cb(results)
|
||||
}
|
||||
|
||||
const handleSelect = (item: ChannelItem) => {
|
||||
if (!localSelected.value.some((c) => c.channelUrl === item.channelUrl)) {
|
||||
localSelected.value.push({ ...item })
|
||||
}
|
||||
searchQuery.value = ''
|
||||
}
|
||||
|
||||
const removeItem = (id: string) => {
|
||||
localSelected.value = localSelected.value.filter((c) => c.id !== id)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -79,105 +171,98 @@ defineExpose({
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dark-dialog {
|
||||
<style>
|
||||
.custom-script-dialog {
|
||||
background-color: #F4F3EB !important;
|
||||
border-radius: 20px !important;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.dark .dark-dialog {
|
||||
.dark .custom-script-dialog {
|
||||
background-color: #1f1f22 !important;
|
||||
}
|
||||
|
||||
.dark-dialog :deep(.el-dialog__header) {
|
||||
margin-right: 0;
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.dark .dark-dialog :deep(.el-dialog__header) {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.dark-dialog :deep(.el-dialog__title) {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #171717;
|
||||
}
|
||||
|
||||
.dark .dark-dialog :deep(.el-dialog__title) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.dark-dialog :deep(.el-dialog__body) {
|
||||
.custom-script-dialog .el-dialog__body {
|
||||
padding: 0 !important;
|
||||
max-height: calc(100vh - 360px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.custom-script-dialog .el-dialog__body::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.custom-script-dialog .el-dialog__body::-webkit-scrollbar-thumb {
|
||||
background-color: #D1CFC7;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.dark .custom-script-dialog .el-dialog__body::-webkit-scrollbar-thumb {
|
||||
background-color: #2a2a2d;
|
||||
}
|
||||
|
||||
.dark-form :deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
color: #4B4B4B !important;
|
||||
}
|
||||
|
||||
.dark .dark-form :deep(.el-form-item__label) {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
.dark-form :deep(.el-input__wrapper) {
|
||||
/* Input styling */
|
||||
.custom-script-dialog .el-input__wrapper,
|
||||
.custom-script-dialog .el-autocomplete .el-input__wrapper {
|
||||
background-color: #EDECE4 !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: none !important;
|
||||
border: 1px solid transparent !important;
|
||||
}
|
||||
|
||||
.dark .dark-form :deep(.el-input__wrapper) {
|
||||
background-color: #222225 !important;
|
||||
}
|
||||
|
||||
.dark-form :deep(.el-input__wrapper.is-focus) {
|
||||
border-color: #3B6DE8 !important;
|
||||
}
|
||||
|
||||
.dark-form :deep(.el-input__inner) {
|
||||
color: #171717 !important;
|
||||
}
|
||||
|
||||
.dark .dark-form :deep(.el-input__inner) {
|
||||
.dark .custom-script-dialog .el-input__wrapper,
|
||||
.dark .custom-script-dialog .el-autocomplete .el-input__wrapper {
|
||||
background-color: #222225 !important;
|
||||
color: #f3f4f6 !important;
|
||||
}
|
||||
|
||||
.dark-form :deep(.el-input__inner::placeholder) {
|
||||
.custom-script-dialog .el-input__wrapper.is-focus,
|
||||
.custom-script-dialog .el-autocomplete .el-input__wrapper.is-focus {
|
||||
border-color: #3B6DE8 !important;
|
||||
}
|
||||
.custom-script-dialog .el-input__inner {
|
||||
color: #171717 !important;
|
||||
}
|
||||
.dark .custom-script-dialog .el-input__inner {
|
||||
color: #f3f4f6 !important;
|
||||
}
|
||||
.custom-script-dialog .el-input__inner::placeholder {
|
||||
color: #99A0AE !important;
|
||||
}
|
||||
|
||||
.dark .dark-form :deep(.el-input__inner::placeholder) {
|
||||
.dark .custom-script-dialog .el-input__inner::placeholder {
|
||||
color: #6b7280 !important;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
padding: 16px 24px 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
/* Autocomplete dropdown */
|
||||
.custom-script-dialog .el-autocomplete-suggestion {
|
||||
background-color: #F4F3EB !important;
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
.dark .custom-script-dialog .el-autocomplete-suggestion {
|
||||
background-color: #1f1f22 !important;
|
||||
}
|
||||
.custom-script-dialog .el-autocomplete-suggestion__wrap {
|
||||
padding: 8px !important;
|
||||
}
|
||||
.custom-script-dialog .el-autocomplete-suggestion__list li {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.custom-script-dialog .el-autocomplete-suggestion__list li:hover {
|
||||
background-color: #E8E6DE !important;
|
||||
}
|
||||
.dark .custom-script-dialog .el-autocomplete-suggestion__list li:hover {
|
||||
background-color: #222225 !important;
|
||||
}
|
||||
|
||||
/* Cancel button */
|
||||
.cancel-btn {
|
||||
background-color: #EDECE4 !important;
|
||||
border-color: transparent !important;
|
||||
color: #4B4B4B !important;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background-color: #E5E4DC !important;
|
||||
color: #171717 !important;
|
||||
}
|
||||
|
||||
.dark .cancel-btn {
|
||||
background-color: #222225 !important;
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
.dark .cancel-btn:hover {
|
||||
background-color: #2a2a2d !important;
|
||||
color: #f3f4f6 !important;
|
||||
|
||||
@@ -25,10 +25,14 @@ import ChatBox from './ChatBox.vue'
|
||||
import TaskCenter from './TaskCenter.vue'
|
||||
import { useChatStore } from '@stores/chat'
|
||||
import { useProviderStore } from '@stores/providers'
|
||||
import { useChannelStore } from '@stores/channel'
|
||||
import { useScriptStore } from '@stores/script'
|
||||
import emitter from '@src/utils/emitter'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const providerStore = useProviderStore()
|
||||
const channelStore = useChannelStore()
|
||||
const scriptStore = useScriptStore()
|
||||
const taskOperationDialog = ref()
|
||||
const addChannelDialog = ref()
|
||||
|
||||
@@ -36,6 +40,8 @@ onMounted(async () => {
|
||||
await providerStore.init()
|
||||
chatStore.loadSessions()
|
||||
chatStore.subscribeToGateway()
|
||||
await scriptStore.fetchScripts()
|
||||
await channelStore.loadSelectedChannels()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -150,7 +150,7 @@ import { javascript } from '@codemirror/lang-javascript';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import type { AutomationScript, ScriptSaveInput } from '@lib/script-types';
|
||||
import { useScriptStore } from '@src/stores/script';
|
||||
import { channels } from '@constant/channel';
|
||||
import { channelDictionary } from '@constant/channel';
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useScriptStore();
|
||||
@@ -197,16 +197,15 @@ watch(() => props.script, (script) => {
|
||||
|
||||
watch(() => form.value.channel, (newUrl) => {
|
||||
if (!newUrl) return;
|
||||
channels.forEach((c: any) => {
|
||||
if (form.value.code.includes(c.channelUrl)) {
|
||||
form.value.code = form.value.code.split(c.channelUrl).join(newUrl);
|
||||
Object.values(channelDictionary).forEach((url) => {
|
||||
if (form.value.code.includes(url)) {
|
||||
form.value.code = form.value.code.split(url).join(newUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getChannelUrl(channel: string): string | undefined {
|
||||
const item = channels.find((c) => c.channelName === channel);
|
||||
return item?.channelUrl;
|
||||
return channelDictionary[channel];
|
||||
}
|
||||
|
||||
async function handleStartRecording() {
|
||||
|
||||
85
src/stores/channel.ts
Normal file
85
src/stores/channel.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useScriptStore } from '@stores/script'
|
||||
import { resolveChannel } from '@constant/channel'
|
||||
import { IPC_EVENTS, CONFIG_KEYS } from '@lib/constants'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export interface ChannelItem {
|
||||
id: string
|
||||
channelName: string
|
||||
channelUrl: string
|
||||
}
|
||||
|
||||
export const useChannelStore = defineStore('channel', () => {
|
||||
const scriptStore = useScriptStore()
|
||||
|
||||
// 用户选中的"一键打开"渠道(持久化到 electron-store)
|
||||
const selectedChannels = ref<ChannelItem[]>([])
|
||||
|
||||
// 从脚本 store 动态聚合可用渠道(按 URL 去重)
|
||||
const availableChannels = computed<ChannelItem[]>(() => {
|
||||
const map = new Map<string, ChannelItem>()
|
||||
for (const script of scriptStore.safeScripts) {
|
||||
if (!script.channel) continue
|
||||
const resolved = resolveChannel(script.channel)
|
||||
const url = resolved.url
|
||||
if (url && !map.has(url)) {
|
||||
map.set(url, {
|
||||
id: uuidv4(),
|
||||
channelName: resolved.name || url,
|
||||
channelUrl: url,
|
||||
})
|
||||
}
|
||||
}
|
||||
return Array.from(map.values())
|
||||
})
|
||||
|
||||
const loadSelectedChannels = async () => {
|
||||
try {
|
||||
const result = await window.api.invoke(IPC_EVENTS.GET_CONFIG, CONFIG_KEYS.SELECTED_CHANNELS)
|
||||
if (Array.isArray(result)) {
|
||||
selectedChannels.value = result
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[channelStore] loadSelectedChannels failed:', err)
|
||||
}
|
||||
selectedChannels.value = []
|
||||
}
|
||||
|
||||
const saveSelectedChannels = async () => {
|
||||
try {
|
||||
// 深拷贝剥离 Vue Proxy,避免 IPC structured clone 失败
|
||||
const payload = JSON.parse(JSON.stringify(selectedChannels.value))
|
||||
await window.api.invoke(IPC_EVENTS.SET_CONFIG, CONFIG_KEYS.SELECTED_CHANNELS, payload)
|
||||
} catch (err) {
|
||||
console.error('[channelStore] saveSelectedChannels failed:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const addSelectedChannel = (item: ChannelItem) => {
|
||||
if (!selectedChannels.value.some((c) => c.channelUrl === item.channelUrl)) {
|
||||
selectedChannels.value.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
const removeSelectedChannel = (id: string) => {
|
||||
selectedChannels.value = selectedChannels.value.filter((c) => c.id !== id)
|
||||
}
|
||||
|
||||
const setSelectedChannels = (items: ChannelItem[]) => {
|
||||
selectedChannels.value = items
|
||||
}
|
||||
|
||||
return {
|
||||
selectedChannels,
|
||||
availableChannels,
|
||||
loadSelectedChannels,
|
||||
saveSelectedChannels,
|
||||
addSelectedChannel,
|
||||
removeSelectedChannel,
|
||||
setSelectedChannels,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user