diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..262321a --- /dev/null +++ b/.env.development @@ -0,0 +1,8 @@ +# 后端 api地址 +VITE_API_BASE_URL=http://192.168.2.22:8101 +# 注册地址 +VITE_REGISTER_API_URL=http://192.168.2.22:48080 +# pk api地址 +VITE_PK_MINI_API_URL=http://192.168.2.22:8086 +# 商店地址 +VITE_SHOP_URL=http://192.168.2.128:8085 diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..07c1b2f --- /dev/null +++ b/.env.production @@ -0,0 +1,8 @@ +# 后端 api地址 +VITE_API_BASE_URL=https://crawlclient.api.yolozs.com +# 注册地址 +VITE_REGISTER_API_URL=https://backstageapi.yolozs.com +# pk api地址 +VITE_PK_MINI_API_URL=https://pk.hanxiaokj.cn +# 商店地址 +VITE_SHOP_URL=https://待填写 diff --git a/src/api/notice.js b/src/api/notice.js index aa332c8..8e6e911 100644 --- a/src/api/notice.js +++ b/src/api/notice.js @@ -2,5 +2,5 @@ import { getAxios } from '@/utils/axios.js' // 获取当前生效的公告列表 export function getActiveNotices() { - return getAxios({ url: '/api/notice/active' }) + return getAxios({ url: '/api/common/notice' }) } diff --git a/src/api/pk-mini.js b/src/api/pk-mini.js index 6cb9250..84d1484 100644 --- a/src/api/pk-mini.js +++ b/src/api/pk-mini.js @@ -4,27 +4,26 @@ */ import axios from 'axios' import { ElMessage } from 'element-plus' +import { ENV } from '@/config' // 创建独立的 axios 实例 const pkAxios = axios.create({ - baseURL: 'http://192.168.2.22:8086/', - // baseURL: 'https://pk.hanxiaokj.cn/', + baseURL: ENV.PK_MINI_API_URL + '/', timeout: 60000, headers: { 'Content-Type': 'application/json' } }) -// 请求拦截器 - 使用主项目的 vvtoken +// 请求拦截器 - PK Mini 后端使用 vvtoken 请求头 pkAxios.interceptors.request.use((config) => { - // 优先使用 vvtoken - const vvtoken = localStorage.getItem('token') - if (vvtoken) { - config.headers['vvtoken'] = vvtoken + const token = localStorage.getItem('token') + if (token) { + config.headers['vvtoken'] = token } else { // 兼容:尝试从 user_data 获取 const userData = JSON.parse(localStorage.getItem('user_data') || '{}') - if (userData.token) { + if (userData.tokenValue) { config.headers['vvtoken'] = userData.tokenValue } } diff --git a/src/api/register.js b/src/api/register.js index 1c037ba..32c1c25 100644 --- a/src/api/register.js +++ b/src/api/register.js @@ -1,4 +1,5 @@ import axios from 'axios' +import { ENV } from '@/config' // 创建独立的 axios 实例,避免被全局拦截器影响 const registerAxios = axios.create({ @@ -7,9 +8,7 @@ const registerAxios = axios.create({ }) // 注册接口使用 tkNewAdmin 后端 -const REGISTER_BASE_URL = process.env.NODE_ENV === 'development' - ? 'http://192.168.2.22:48080' - : 'https://backstageapi.yolozs.com' +const REGISTER_BASE_URL = ENV.REGISTER_API_URL /** * 租户注册 diff --git a/src/assets/placeholder-webai.png b/src/assets/placeholder-webai.png index 72ca72e..79a95a6 100644 Binary files a/src/assets/placeholder-webai.png and b/src/assets/placeholder-webai.png differ diff --git a/src/components/HostListDialog.vue b/src/components/HostListDialog.vue index 89ef770..31bc431 100644 --- a/src/components/HostListDialog.vue +++ b/src/components/HostListDialog.vue @@ -16,6 +16,10 @@
+
+ + +
+
+
+

添加主播

+ +
+ +
+ +
+ + +
+ 已输入 {{ parsedIds.length }} 个ID +
+
+ + +
+ +
+ + +
+
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ + {{ addStatus.message }} + + +
+ + +
+
+
+
@@ -231,6 +313,57 @@ const maxCount = ref(100) const selectedLevels = ref(new Set()) const showLevelDropdown = ref(false) +// 添加主播弹窗状态 +const showAddDialog = ref(false) +const addLoading = ref(false) +const addStatus = ref(null) +const addForm = ref({ + idsText: '', + invitationType: '1', + country: '美国', + hostsLevel: 'A1', +}) + +// 国家选项 +const COUNTRY_OPTIONS = [ + { label: '美国', value: '美国', eng: 'United States' }, + { label: '英国', value: '英国', eng: 'United Kingdom' }, + { label: '加拿大', value: '加拿大', eng: 'Canada' }, + { label: '澳大利亚', value: '澳大利亚', eng: 'Australia' }, + { label: '德国', value: '德国', eng: 'Germany' }, + { label: '法国', value: '法国', eng: 'France' }, + { label: '日本', value: '日本', eng: 'Japan' }, + { label: '韩国', value: '韩国', eng: 'South Korea' }, + { label: '巴西', value: '巴西', eng: 'Brazil' }, + { label: '印度尼西亚', value: '印度尼西亚', eng: 'Indonesia' }, + { label: '墨西哥', value: '墨西哥', eng: 'Mexico' }, + { label: '菲律宾', value: '菲律宾', eng: 'Philippines' }, + { label: '越南', value: '越南', eng: 'Vietnam' }, + { label: '泰国', value: '泰国', eng: 'Thailand' }, + { label: '马来西亚', value: '马来西亚', eng: 'Malaysia' }, + { label: '沙特阿拉伯', value: '沙特阿拉伯', eng: 'Saudi Arabia' }, + { label: '西班牙', value: '西班牙', eng: 'Spain' }, + { label: '意大利', value: '意大利', eng: 'Italy' }, + { label: '土耳其', value: '土耳其', eng: 'Turkey' }, + { label: '埃及', value: '埃及', eng: 'Egypt' }, + { label: '尼日利亚', value: '尼日利亚', eng: 'Nigeria' }, + { label: '哥伦比亚', value: '哥伦比亚', eng: 'Colombia' }, + { label: '阿根廷', value: '阿根廷', eng: 'Argentina' }, + { label: '智利', value: '智利', eng: 'Chile' }, + { label: '秘鲁', value: '秘鲁', eng: 'Peru' }, + { label: '以色列', value: '以色列', eng: 'Israel' }, + { label: '伊拉克', value: '伊拉克', eng: 'Iraq' }, + { label: '约旦', value: '约旦', eng: 'Jordan' }, +] + +// 解析输入的主播ID列表 +const parsedIds = computed(() => { + return addForm.value.idsText + .split('\n') + .map(line => line.trim()) + .filter(id => id.length > 0) +}) + // Lifecycle watch(() => props.visible, (newVal) => { if (newVal) { @@ -273,6 +406,7 @@ const loadHosts = async () => { if (!isElectron()) return try { const data = await window.electronAPI.loadAnchorData() + console.log('加载主播数据:', data) hosts.value = data selected.value = new Set() } catch (e) { @@ -413,22 +547,89 @@ const invertSelect = () => { selected.value = next } -const deleteSelected = () => { +const deleteSelected = async () => { if (!selected.value.size) return if (!confirm(`确认删除选中的 ${selected.value.size} 项吗?`)) return - const remaining = hosts.value.filter(h => !selected.value.has(h.anchorId)) - hosts.value = remaining - selected.value = new Set() + if (isElectron()) { + try { + const ids = Array.from(selected.value) + const result = await window.electronAPI.deleteAnchorData(ids) + if (result.success) { + console.log('[HostListDialog] 删除成功,重新加载数据') + await loadHosts() + } else { + console.error('[HostListDialog] 删除失败:', result.error) + // fallback: 前端本地删除 + hosts.value = hosts.value.filter(h => !selected.value.has(h.anchorId)) + selected.value = new Set() + } + } catch (e) { + console.error('[HostListDialog] 删除异常:', e) + hosts.value = hosts.value.filter(h => !selected.value.has(h.anchorId)) + selected.value = new Set() + } + } else { + hosts.value = hosts.value.filter(h => !selected.value.has(h.anchorId)) + selected.value = new Set() + } } const onClose = () => emit('close') const handleSave = async () => { - if (isElectron()) { - await window.electronAPI.saveAnchorData(JSON.parse(JSON.stringify(hosts.value))) - } emit('save', hosts.value) onClose() } + +// 关闭添加弹窗 +const closeAddDialog = () => { + showAddDialog.value = false + addForm.value = { idsText: '', invitationType: '1', country: '美国', hostsLevel: 'A1' } + addStatus.value = null + addLoading.value = false +} + +// 批量添加主播 +const handleAddHosts = async () => { + const ids = parsedIds.value + if (ids.length === 0) return + + addLoading.value = true + addStatus.value = null + + // 查找国家英文名 + const countryObj = COUNTRY_OPTIONS.find(c => c.value === addForm.value.country) + + // 构造批量记录 + const records = ids.map(hostsId => ({ + hostsId, + invitationType: addForm.value.invitationType, + country: addForm.value.country || null, + countryEng: countryObj?.eng || null, + hostsLevel: addForm.value.hostsLevel || null, + createTime: Date.now(), + })) + + if (isElectron()) { + try { + const result = await window.electronAPI.addAnchorData(records) + if (result.success) { + addStatus.value = { type: 'success', message: `成功导入 ${ids.length} 个主播` } + // 重新加载列表 + await loadHosts() + // 延迟关闭弹窗 + setTimeout(() => closeAddDialog(), 1000) + } else { + addStatus.value = { type: 'error', message: result.error || '导入失败' } + } + } catch (e) { + console.error('[HostListDialog] 添加主播失败:', e) + addStatus.value = { type: 'error', message: '导入异常: ' + String(e) } + } + } else { + addStatus.value = { type: 'error', message: '非 Electron 环境,无法添加' } + } + addLoading.value = false +} diff --git a/src/components/NoticeBar.vue b/src/components/NoticeBar.vue index cc3263f..3471a50 100644 --- a/src/components/NoticeBar.vue +++ b/src/components/NoticeBar.vue @@ -1,19 +1,20 @@ diff --git a/src/components/pk-mini/mine/PKRecord.vue b/src/components/pk-mini/mine/PKRecord.vue index e049a42..2fbaa6a 100644 --- a/src/components/pk-mini/mine/PKRecord.vue +++ b/src/components/pk-mini/mine/PKRecord.vue @@ -1,8 +1,8 @@ @@ -232,6 +232,28 @@ onMounted(() => { height: 100%; } +.pk-layout { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; +} + +.left-panel { + flex: 1; + min-width: 0; + height: 100%; + overflow: hidden; +} + +.right-panel { + width: 380px; + flex-shrink: 0; + height: 100%; + overflow: hidden; + border-left: 1px solid #03aba82f; +} + .demo-panel { width: 100%; height: 100%; @@ -378,53 +400,54 @@ onMounted(() => { display: flex; flex-direction: column; align-items: center; - padding: 20px; - border-left: 1px solid #03aba82f; + padding: 15px; + box-sizing: border-box; + overflow: hidden; } .detail-avatars { display: flex; - gap: 40px; - margin-bottom: 20px; + gap: 20px; + margin-bottom: 12px; } .detail-avatar { - width: 70px; - height: 70px; + width: 50px; + height: 50px; border-radius: 50%; object-fit: cover; } .detail-total { width: 100%; - margin-bottom: 20px; + margin-bottom: 12px; } .total-card { display: flex; align-items: center; justify-content: space-around; - padding: 15px; + padding: 10px; background: linear-gradient(90deg, #e4ffff, #fff, #e4ffff); border-radius: 30px; } .total-num { - font-size: 16px; + font-size: 13px; font-weight: bold; color: #333; } .total-icon { - width: 35px; - height: 28px; + width: 28px; + height: 22px; } .detail-rounds { flex: 1; width: 100%; display: flex; - gap: 15px; + gap: 10px; overflow: hidden; } @@ -432,9 +455,9 @@ onMounted(() => { flex: 1; display: flex; flex-direction: column; - gap: 10px; - padding: 15px; - border-radius: 16px; + gap: 8px; + padding: 10px; + border-radius: 12px; overflow: auto; } @@ -449,9 +472,9 @@ onMounted(() => { } .round-item { - padding: 12px 15px; + padding: 8px 10px; border-radius: 8px; - font-size: 16px; + font-size: 13px; font-weight: bold; color: #03aba8; text-align: center; diff --git a/src/config/index.js b/src/config/index.js new file mode 100644 index 0000000..13242f9 --- /dev/null +++ b/src/config/index.js @@ -0,0 +1,10 @@ +export const ENV = { + // 主 API 地址 + API_BASE_URL: import.meta.env.VITE_API_BASE_URL, + // 注册 API 地址(tkNewAdmin 后端) + REGISTER_API_URL: import.meta.env.VITE_REGISTER_API_URL, + // PK Mini API 地址 + PK_MINI_API_URL: import.meta.env.VITE_PK_MINI_API_URL, + // YOLO 商店 iframe 地址 + SHOP_URL: import.meta.env.VITE_SHOP_URL, +} diff --git a/src/config/pk-mini.js b/src/config/pk-mini.js index bcaec18..f86a48a 100644 --- a/src/config/pk-mini.js +++ b/src/config/pk-mini.js @@ -3,20 +3,22 @@ * * GoEasy 续费后,将 GOEASY_ENABLED 改为 true 即可开启消息功能 */ +import { ENV } from '@/config' export const PK_MINI_CONFIG = { // GoEasy 开关 - 续费后改为 true - GOEASY_ENABLED: false, + GOEASY_ENABLED: true, // GoEasy 配置 GOEASY: { HOST: 'hangzhou.goeasy.io', - APP_KEY: 'BC-a88037e060ed4753bb316ac7239e62d9', + APP_KEY: 'PC-a88037e060ed4753bb316ac7239e62d9', }, - // API 基础地址 - API_BASE_URL: 'http://192.168.2.22:8086/', - // API_BASE_URL: 'https://pk.hanxiaokj.cn/', + // API 基础地址(从中心配置读取,随环境自动切换) + get API_BASE_URL() { + return ENV.PK_MINI_API_URL + '/' + }, // 头像 CDN 地址 AVATAR_CDN_PREFIX: 'https://vv-1317974657.cos.ap-shanghai.myqcloud.com/headerIcon/', diff --git a/src/layout/WorkbenchLayout.vue b/src/layout/WorkbenchLayout.vue index 6c285e6..70a8de2 100644 --- a/src/layout/WorkbenchLayout.vue +++ b/src/layout/WorkbenchLayout.vue @@ -142,7 +142,7 @@
@@ -161,6 +161,7 @@ import ConfigPage from '@/pages/ConfigPage.vue' import FanWorkbench from '@/views/tk/FanWorkbench.vue' import PkMiniWorkbench from '@/views/pk-mini/PkMiniWorkbench.vue' import PermissionMask from '@/components/PermissionMask.vue' +import { ENV } from '@/config' // 占位图片 - 无权限时显示的工作台截图 import placeholderTk from '@/assets/placeholder-tk.png' @@ -173,6 +174,7 @@ const emit = defineEmits(['logout', 'go-back', 'stop-all']) const currentView = ref('tk') // Default Tab const autoDmMode = ref('config') // Default Sub-state: 'config' or 'browser' const adminLoaded = ref(false) // 懒加载:首次切换到管理后台时才加载 iframe +const shopUrl = ENV.SHOP_URL const handleGoToBrowser = async () => { autoDmMode.value = 'browser' diff --git a/src/locales/zh.js b/src/locales/zh.js index bc838df..b86ef90 100644 --- a/src/locales/zh.js +++ b/src/locales/zh.js @@ -244,6 +244,23 @@ export default { save: '保存', loading: '加载中...', noData: '暂无数据', - noNotice: '暂无站内信' + noNotice: '暂无站内信', + // MiniPKMessage 邀请卡片 + man: '男', + woman: '女', + PKTime: 'PK时间:', + GoldCoin: '金币:', + match: '场', + Note: '备注:', + agree: '同意', + Refuse: '拒绝', + HaveAgreedToTheInvitation: '已同意邀请', + HaveRefusedTheInvitation: '已拒绝邀请', + WaitForTheOtherPartyResponse: '等待对方响应', + Hint: '提示', + AfterASuccessfulInvitationThePKCannotBeModifiedOrDeletedPleaseOperateWithCaution: '同意后PK邀请将无法修改或删除,请谨慎操作', + AreYouSureYouWantToDeclineThisInvitation: '确定要拒绝此邀请吗?', + Cancel: '取消', + Confirm: '确认' } } \ No newline at end of file diff --git a/src/pages/LoginPage.vue b/src/pages/LoginPage.vue index c91acfa..0c8f1c9 100644 --- a/src/pages/LoginPage.vue +++ b/src/pages/LoginPage.vue @@ -29,8 +29,8 @@
- -
+ +
Logo
diff --git a/src/stores/noticeStore.js b/src/stores/noticeStore.js index 47fd93e..f1180eb 100644 --- a/src/stores/noticeStore.js +++ b/src/stores/noticeStore.js @@ -8,18 +8,29 @@ export const useNoticeStore = defineStore('notice', () => { const isLoading = ref(false) const dismissedIds = ref([]) // 当前会话已关闭的公告 ID const lastFetchTime = ref(null) - const useMock = ref(true) // 后台接口就绪后改为 false + const useMock = ref(false) // 后台接口就绪后改为 false // 过滤已关闭公告后的有效列表 const activeNotices = computed(() => notices.value.filter(n => !dismissedIds.value.includes(n.id)) ) + // info 或无 category 的公告 → 滚动栏显示 title + const infoNotices = computed(() => + activeNotices.value.filter(n => !n.category || n.category === 'info') + ) + + // danger / warning 的公告 → 弹窗显示 title + content + const alertNotices = computed(() => + activeNotices.value.filter(n => n.category === 'danger' || n.category === 'warning') + ) + // 是否有可显示的公告 const hasNotices = computed(() => activeNotices.value.length > 0) /** * 从后台拉取公告 + * 全局 axios 拦截器在 code==0 时返回 response.data.data,即数组本身 */ const fetchNotices = async () => { if (isLoading.value) return @@ -31,10 +42,9 @@ export const useNoticeStore = defineStore('notice', () => { isLoading.value = true try { const res = await getActiveNotices() - if (res && res.data) { - notices.value = res.data - lastFetchTime.value = Date.now() - } + console.log('[NoticeStore] 获取公告', res) + notices.value = Array.isArray(res) ? res : [] + lastFetchTime.value = Date.now() } catch (error) { console.error('[NoticeStore] 获取公告失败:', error) } finally { @@ -56,8 +66,9 @@ export const useNoticeStore = defineStore('notice', () => { */ const loadMockNotices = () => { notices.value = [ - { id: 1, content: '欢迎使用 Yolo 系统,如有问题请联系管理员。', type: 'info' }, - { id: 2, content: '系统将于本周六凌晨 2:00-4:00 进行维护升级,届时服务将暂停,请提前做好安排。', type: 'warning' }, + { id: 1, title: 'YOLO 系统公告', content: '

欢迎使用 Yolo 系统,如有问题请联系管理员。

', category: 'info' }, + { id: 2, title: '系统维护通知', content: '

系统将于本周六凌晨 2:00-4:00 进行维护升级,届时服务将暂停,请提前做好安排。

', category: 'warning' }, + { id: 3, title: '紧急安全通知', content: '

请所有用户立即更新客户端至最新版本。

', category: 'danger' }, ] lastFetchTime.value = Date.now() } @@ -66,6 +77,8 @@ export const useNoticeStore = defineStore('notice', () => { // 状态 notices, activeNotices, + infoNotices, + alertNotices, hasNotices, isLoading, useMock, diff --git a/src/stores/pk-mini/notice.js b/src/stores/pk-mini/notice.js index 4ffdb36..c629520 100644 --- a/src/stores/pk-mini/notice.js +++ b/src/stores/pk-mini/notice.js @@ -43,3 +43,20 @@ export const pkIMloginStore = defineStore('pkIMlogin', { } } }) + +export const pkUnreadStore = defineStore('pkUnread', { + state: () => { + return { count: 0 } + }, + actions: { + setCount(count) { + this.count = count + }, + decrease(num = 1) { + this.count = Math.max(0, this.count - num) + }, + clear() { + this.count = 0 + } + } +}) diff --git a/src/utils/axios.js b/src/utils/axios.js index 7e6e67d..643b163 100644 --- a/src/utils/axios.js +++ b/src/utils/axios.js @@ -8,24 +8,13 @@ import router from '@/router' import { ElMessage } from 'element-plus'; import { usePythonBridge, } from '@/utils/pythonBridge' +import { ENV } from '@/config' const { stopScript } = usePythonBridge(); // 请求地址前缀 -let baseURL = '' -if (process.env.NODE_ENV === 'development') { - // 生产环境 - // baseURL = "https://api.tkpage.yolozs.com" - baseURL = "http://192.168.2.22:8101" - // baseURL = "https://crawlclient.api.yolozs.com" -} else { - // 测试环境 - // baseURL = "http://120.26.251.180:8085/" - // 开发环境 - baseURL = "https://crawlclient.api.yolozs.com" - // baseURL = "http://api.tkpage.vvtiktok.cn" -} +const baseURL = ENV.API_BASE_URL // 请求拦截器 axios.interceptors.request.use((config) => { diff --git a/src/utils/pk-mini/goeasy.js b/src/utils/pk-mini/goeasy.js index e468191..ff27394 100644 --- a/src/utils/pk-mini/goeasy.js +++ b/src/utils/pk-mini/goeasy.js @@ -96,6 +96,7 @@ export function goEasyGetConversations() { return new Promise((resolve, reject) => { im.latestConversations({ onSuccess: function (result) { + console.log('会话列表', result) resolve(result) }, onFailed: function (error) { diff --git a/src/views/pk-mini/Message.vue b/src/views/pk-mini/Message.vue index 057fb6b..86b1162 100644 --- a/src/views/pk-mini/Message.vue +++ b/src/views/pk-mini/Message.vue @@ -1,9 +1,9 @@