mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
Compare commits
2 Commits
55e937bf2f
...
2966a49fc9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2966a49fc9 | ||
|
|
45338ffe7d |
3
.github/workflows/bun.yml
vendored
3
.github/workflows/bun.yml
vendored
@@ -32,9 +32,6 @@ jobs:
|
|||||||
- name: 📦 Build
|
- name: 📦 Build
|
||||||
run: bun run build
|
run: bun run build
|
||||||
|
|
||||||
- name: 📦 Upload SourceMap
|
|
||||||
run: bunx @hyperdx/cli upload-sourcemaps --serviceKey ${{ secrets.HYPERDX_SERVICE_KEY }} --path dist/assets
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
10
default.d.ts
vendored
10
default.d.ts
vendored
@@ -31,3 +31,13 @@ declare global {
|
|||||||
$dialog: DialogProviderInst
|
$dialog: DialogProviderInst
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vite worker 与样式类型声明
|
||||||
|
declare module '*?worker' {
|
||||||
|
const workerConstructor: { new(): Worker }
|
||||||
|
export default workerConstructor
|
||||||
|
}
|
||||||
|
declare module '*.css' {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"knip": "knip"
|
"knip": "knip"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@guolao/vue-monaco-editor": "^1.5.5",
|
|
||||||
"@hyperdx/browser": "^0.21.2",
|
"@hyperdx/browser": "^0.21.2",
|
||||||
"@hyperdx/cli": "^0.1.0",
|
"@hyperdx/cli": "^0.1.0",
|
||||||
"@microsoft/signalr": "^9.0.6",
|
"@microsoft/signalr": "^9.0.6",
|
||||||
@@ -52,6 +51,7 @@
|
|||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"linqts": "^3.2.0",
|
"linqts": "^3.2.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"monaco-editor": "^0.53.0",
|
"monaco-editor": "^0.53.0",
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
"unplugin-vue-markdown": "^29.2.0",
|
"unplugin-vue-markdown": "^29.2.0",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vite": "npm:rolldown-vite@latest",
|
"vite": "npm:rolldown-vite@latest",
|
||||||
|
"vite-plugin-monaco-editor-nls": "^3.0.1",
|
||||||
"vite-svg-loader": "^5.1.0",
|
"vite-svg-loader": "^5.1.0",
|
||||||
"vue": "3.5.22",
|
"vue": "3.5.22",
|
||||||
"vue-cropperjs": "^5.0.0",
|
"vue-cropperjs": "^5.0.0",
|
||||||
@@ -94,6 +95,7 @@
|
|||||||
"stylus": "^0.64.0",
|
"stylus": "^0.64.0",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite-plugin-cdn-import": "^1.0.1",
|
"vite-plugin-cdn-import": "^1.0.1",
|
||||||
|
"vscode-loc": "git+https://github.com/microsoft/vscode-loc.git",
|
||||||
"vue-vine": "^1.7.6"
|
"vue-vine": "^1.7.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/App.vue
14
src/App.vue
@@ -12,16 +12,18 @@ import {
|
|||||||
NSpin,
|
NSpin,
|
||||||
zhCN,
|
zhCN,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { computed } from 'vue'
|
import { computed, defineAsyncComponent } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import ManageLayout from '@/views/ManageLayout.vue'
|
|
||||||
import ViewerLayout from '@/views/ViewerLayout.vue'
|
|
||||||
import { ThemeType } from './api/api-models'
|
import { ThemeType } from './api/api-models'
|
||||||
import ClientLayout from './client/ClientLayout.vue'
|
|
||||||
import TempComponent from './components/TempComponent.vue'
|
import TempComponent from './components/TempComponent.vue'
|
||||||
import { isDarkMode, theme } from './Utils'
|
import { isDarkMode, theme } from './Utils'
|
||||||
import OBSLayout from './views/OBSLayout.vue'
|
|
||||||
import OpenLiveLayout from './views/OpenLiveLayout.vue'
|
// 将大型布局组件改为异步组件,避免打入入口包
|
||||||
|
const ManageLayout = defineAsyncComponent(() => import('@/views/ManageLayout.vue'))
|
||||||
|
const ViewerLayout = defineAsyncComponent(() => import('@/views/ViewerLayout.vue'))
|
||||||
|
const ClientLayout = defineAsyncComponent(() => import('./client/ClientLayout.vue'))
|
||||||
|
const OBSLayout = defineAsyncComponent(() => import('./views/OBSLayout.vue'))
|
||||||
|
const OpenLiveLayout = defineAsyncComponent(() => import('./views/OpenLiveLayout.vue'))
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const themeType = useStorage('Settings.Theme', ThemeType.Auto)
|
const themeType = useStorage('Settings.Theme', ThemeType.Auto)
|
||||||
|
|||||||
@@ -1,66 +1,121 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { editor } from 'monaco-editor' // 全部导入
|
import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import * as monaco from 'monaco-editor'
|
||||||
|
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
|
||||||
|
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
|
||||||
|
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
|
||||||
|
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
|
||||||
|
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
|
||||||
|
|
||||||
const { language, height = 400 } = defineProps<{
|
|
||||||
|
const { language, height = 400, theme = 'vs-dark', options, path } = defineProps<{
|
||||||
language: string
|
language: string
|
||||||
height?: number
|
height?: number
|
||||||
|
theme?: string
|
||||||
|
options?: Record<string, any>
|
||||||
|
path?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const value = defineModel<string>('value')
|
const value = defineModel<string>('value')
|
||||||
|
|
||||||
const editorContainer = ref<HTMLElement>()
|
// 在创建编辑器前确保容器存在
|
||||||
let editorInstance: editor.IStandaloneCodeEditor | null = null
|
const ready = ref(false)
|
||||||
|
const initError = ref<string | null>(null)
|
||||||
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
|
let editor: monaco.editor.IStandaloneCodeEditor | null = null
|
||||||
|
let model: monaco.editor.ITextModel | null = null
|
||||||
|
let createdModel = false
|
||||||
|
|
||||||
onMounted(() => {
|
// 配置 Monaco Environment
|
||||||
if (!editorContainer.value) return
|
;(self as any).MonacoEnvironment = {
|
||||||
|
getWorker(_: string, label: string) {
|
||||||
editorInstance = editor.create(editorContainer.value, {
|
if (label === 'json') {
|
||||||
value: value.value,
|
return new jsonWorker()
|
||||||
language,
|
}
|
||||||
minimap: {
|
if (label === 'css' || label === 'scss' || label === 'less') {
|
||||||
enabled: true,
|
return new cssWorker()
|
||||||
|
}
|
||||||
|
if (label === 'html' || label === 'handlebars' || label === 'razor') {
|
||||||
|
return new htmlWorker()
|
||||||
|
}
|
||||||
|
if (label === 'typescript' || label === 'javascript') {
|
||||||
|
return new tsWorker()
|
||||||
|
}
|
||||||
|
return new editorWorker()
|
||||||
},
|
},
|
||||||
colorDecorators: true,
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const uri = monaco.Uri.parse(
|
||||||
|
path ?? `inmemory://model/${crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)}.${language ?? 'txt'}`,
|
||||||
|
)
|
||||||
|
model = monaco.editor.getModel(uri)
|
||||||
|
if (!model) {
|
||||||
|
model = monaco.editor.createModel(value?.value ?? '', language, uri)
|
||||||
|
createdModel = true
|
||||||
|
}
|
||||||
|
|
||||||
|
editor = monaco.editor.create(containerRef.value!, {
|
||||||
|
model,
|
||||||
|
theme,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
|
...(options ?? {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
editorInstance.onDidChangeModelContent(() => {
|
// 同步 model -> v-model
|
||||||
if (editorInstance) {
|
editor.onDidChangeModelContent(() => {
|
||||||
const currentValue = editorInstance.getValue()
|
const current = model!.getValue()
|
||||||
if (currentValue !== value.value) {
|
if (current !== value.value) value.value = current
|
||||||
value.value = currentValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ready.value = true
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Monaco 初始化失败:', err)
|
||||||
|
initError.value = (err as Error)?.message ?? String(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// v-model 变更 -> model
|
||||||
|
watch(value, (nv) => {
|
||||||
|
if (model && typeof nv === 'string' && nv !== model.getValue()) {
|
||||||
|
model.setValue(nv)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 语言切换
|
||||||
|
watch(() => language, (lang) => {
|
||||||
|
if (model && lang) {
|
||||||
|
monaco.editor.setModelLanguage(model, lang)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 主题切换
|
||||||
|
watch(() => theme, (t) => {
|
||||||
|
if (editor && t) {
|
||||||
|
editor.updateOptions({ theme: t })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (editorInstance) {
|
try {
|
||||||
editorInstance.dispose()
|
editor?.dispose()
|
||||||
editorInstance = null
|
// 仅销毁我们创建的临时 model,避免复用路径时把共享 model 误删
|
||||||
}
|
if (createdModel && model) {
|
||||||
})
|
model.dispose()
|
||||||
|
|
||||||
watch(value, (newValue) => {
|
|
||||||
if (editorInstance && newValue !== editorInstance.getValue()) {
|
|
||||||
editorInstance.setValue(newValue ?? '')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => language, (newLang) => {
|
|
||||||
if (editorInstance) {
|
|
||||||
const model = editorInstance.getModel()
|
|
||||||
if (model) {
|
|
||||||
editor.setModelLanguage(model, newLang)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch {}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div :style="`height: ${height}px; width: 100%; position: relative;`">
|
||||||
ref="editorContainer"
|
<div v-if="!ready" :style="`position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color: var(--text-color, #888); text-align:center; padding:8px;`">
|
||||||
:style="`height: ${height}px;`"
|
<div>
|
||||||
/>
|
<div>正在加载编辑器…</div>
|
||||||
|
<div v-if="initError" style="margin-top:6px; color:#d9534f; font-size:12px;">{{ initError }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref="containerRef" :style="`height: ${height}px; width: 100%;`"></div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -20,24 +20,34 @@ const themeVars = useThemeVars()
|
|||||||
|
|
||||||
const weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
const weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
||||||
|
|
||||||
|
// 常量定义
|
||||||
|
const MILLISECONDS_PER_DAY = 86400000
|
||||||
|
|
||||||
function getISOWeek(date: Date) {
|
function getISOWeek(date: Date) {
|
||||||
const target = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
|
const target = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
|
||||||
const dayNumber = target.getUTCDay() || 7
|
const dayNumber = target.getUTCDay() || 7
|
||||||
target.setUTCDate(target.getUTCDate() + 4 - dayNumber)
|
target.setUTCDate(target.getUTCDate() + 4 - dayNumber)
|
||||||
const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1))
|
const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1))
|
||||||
const week = Math.ceil(((target.getTime() - yearStart.getTime()) / 86400000 + 1) / 7)
|
const week = Math.ceil(((target.getTime() - yearStart.getTime()) / MILLISECONDS_PER_DAY + 1) / 7)
|
||||||
return {
|
return {
|
||||||
year: target.getUTCFullYear(),
|
year: target.getUTCFullYear(),
|
||||||
week,
|
week,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentISOWeek = getISOWeek(new Date())
|
const now = new Date()
|
||||||
|
const currentISOWeek = getISOWeek(now)
|
||||||
|
const currentDayOfWeek = (now.getDay() + 6) % 7 // 转换为周一=0的格式
|
||||||
|
|
||||||
function isCurrentWeek(year: number, week: number) {
|
function isCurrentWeek(year: number, week: number) {
|
||||||
return year === currentISOWeek.year && week === currentISOWeek.week
|
return year === currentISOWeek.year && week === currentISOWeek.week
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCurrentDay(year: number, week: number, dayIndex: number) {
|
||||||
|
if (!isCurrentWeek(year, week)) return false
|
||||||
|
return dayIndex === currentDayOfWeek
|
||||||
|
}
|
||||||
|
|
||||||
const dateFormatter = new Intl.DateTimeFormat('zh-CN', {
|
const dateFormatter = new Intl.DateTimeFormat('zh-CN', {
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
@@ -51,17 +61,40 @@ function getWeekRangeLabel(year: number, week: number) {
|
|||||||
|
|
||||||
function getDateFromWeek(year: number, week: number, dayOfWeek: number): Date {
|
function getDateFromWeek(year: number, week: number, dayOfWeek: number): Date {
|
||||||
// week starts from 1-52, dayOfWeek starts from 0-6 where 0 is Monday
|
// week starts from 1-52, dayOfWeek starts from 0-6 where 0 is Monday
|
||||||
const simple = new Date(year, 0, 1 + (week - 1) * 7)
|
const januaryFourth = new Date(year, 0, 4)
|
||||||
const dow = simple.getDay()
|
const startOfWeekOne = new Date(januaryFourth)
|
||||||
const ISOweekStart = simple
|
const dayOfWeekJan4 = (januaryFourth.getDay() + 6) % 7
|
||||||
if (dow <= 4) ISOweekStart.setDate(simple.getDate() - simple.getDay() + 1)
|
startOfWeekOne.setDate(januaryFourth.getDate() - dayOfWeekJan4)
|
||||||
else ISOweekStart.setDate(simple.getDate() + 8 - simple.getDay())
|
|
||||||
return new Date(ISOweekStart.getFullYear(), ISOweekStart.getMonth(), ISOweekStart.getDate() + dayOfWeek)
|
const targetDate = new Date(startOfWeekOne)
|
||||||
|
targetDate.setDate(startOfWeekOne.getDate() + (week - 1) * 7 + dayOfWeek)
|
||||||
|
return targetDate
|
||||||
|
}
|
||||||
|
|
||||||
|
// 样式工具函数
|
||||||
|
function getDayHeaderStyle(year: number, week: number, dayIndex: number, primaryColor: string, primaryColorSuppl: string) {
|
||||||
|
const isToday = isCurrentDay(year, week, dayIndex)
|
||||||
|
|
||||||
|
return {
|
||||||
|
marginBottom: '6px',
|
||||||
|
padding: '4px 8px',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '600',
|
||||||
|
background: isToday
|
||||||
|
? `linear-gradient(135deg, ${primaryColor}25 0%, ${primaryColor}40 100%)`
|
||||||
|
: `linear-gradient(135deg, ${primaryColorSuppl}15 0%, ${primaryColorSuppl}25 100%)`,
|
||||||
|
borderRadius: '4px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
boxShadow: isToday ? `0 0 0 2px ${primaryColor}66` : undefined,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NEmpty v-if="(schedules?.length ?? 0) == 0" />
|
<NEmpty v-if="(schedules?.length ?? 0) === 0" />
|
||||||
<NList
|
<NList
|
||||||
v-else
|
v-else
|
||||||
style="padding: 0"
|
style="padding: 0"
|
||||||
@@ -156,24 +189,25 @@ function getDateFromWeek(year: number, week: number, dayOfWeek: number): Date {
|
|||||||
>
|
>
|
||||||
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
|
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
|
||||||
<div
|
<div
|
||||||
:style="{
|
:style="getDayHeaderStyle(item.year, item.week, index, themeVars.primaryColor, themeVars.primaryColorSuppl)"
|
||||||
marginBottom: '6px',
|
|
||||||
padding: '4px 8px',
|
|
||||||
fontSize: '13px',
|
|
||||||
fontWeight: '600',
|
|
||||||
background: `linear-gradient(135deg, ${themeVars.primaryColorSuppl}15 0%, ${themeVars.primaryColorSuppl}25 100%)`,
|
|
||||||
borderRadius: '4px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '6px',
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<NTime
|
<NTime
|
||||||
:time="getDateFromWeek(item.year, item.week, index)"
|
:time="getDateFromWeek(item.year, item.week, index)"
|
||||||
format="MM/dd"
|
format="MM/dd"
|
||||||
:style="{ color: themeVars.primaryColor }"
|
:style="{
|
||||||
|
color: isCurrentDay(item.year, item.week, index) ? themeVars.primaryColor : themeVars.primaryColorSuppl,
|
||||||
|
fontWeight: isCurrentDay(item.year, item.week, index) ? '700' : '600',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<NText :style="{ fontWeight: isCurrentDay(item.year, item.week, index) ? '700' : '500' }">
|
||||||
|
{{ weekdays[index] }}
|
||||||
|
</NText>
|
||||||
|
<NBadge
|
||||||
|
v-if="isCurrentDay(item.year, item.week, index)"
|
||||||
|
dot
|
||||||
|
:color="themeVars.primaryColor"
|
||||||
|
:style="{ marginLeft: 'auto' }"
|
||||||
/>
|
/>
|
||||||
<NText>{{ weekdays[index] }}</NText>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="flex: 1; display: flex; flex-direction: column; min-height: 65px;">
|
<div style="flex: 1; display: flex; flex-direction: column; min-height: 65px;">
|
||||||
<NCard
|
<NCard
|
||||||
@@ -181,8 +215,10 @@ function getDateFromWeek(year: number, week: number, dayOfWeek: number): Date {
|
|||||||
size="small"
|
size="small"
|
||||||
:style="{
|
:style="{
|
||||||
minHeight: '40px',
|
minHeight: '40px',
|
||||||
background: `linear-gradient(135deg, ${themeVars.cardColor} 0%, ${themeVars.bodyColor} 100%)`,
|
background: isCurrentDay(item.year, item.week, index)
|
||||||
border: `1px dashed ${themeVars.dividerColor}`,
|
? `linear-gradient(135deg, ${themeVars.primaryColorSuppl}08 0%, ${themeVars.primaryColorSuppl}15 100%)`
|
||||||
|
: `linear-gradient(135deg, ${themeVars.cardColor} 0%, ${themeVars.bodyColor} 100%)`,
|
||||||
|
border: `1px dashed ${isCurrentDay(item.year, item.week, index) ? themeVars.primaryColorSuppl : themeVars.dividerColor}`,
|
||||||
cursor: isSelf ? 'pointer' : 'default',
|
cursor: isSelf ? 'pointer' : 'default',
|
||||||
transition: 'all 0.2s ease',
|
transition: 'all 0.2s ease',
|
||||||
}"
|
}"
|
||||||
|
|||||||
@@ -203,85 +203,166 @@ const emptyCover = `${IMGUR_URL}None.png`
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.goods-card {
|
.goods-card {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
||||||
|
transition: left 0.5s ease;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card:hover::before {
|
||||||
|
left: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-card:hover {
|
.goods-card:hover {
|
||||||
transform: translateY(-3px);
|
transform: translateY(-6px);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card:active {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
transition: all 0.1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pinned-card {
|
.pinned-card {
|
||||||
border: 2px solid var(--primary-color);
|
border: 2px solid var(--primary-color);
|
||||||
box-shadow: 0 2px 10px rgba(var(--primary-color-rgb), 0.15);
|
box-shadow: 0 4px 16px rgba(24, 160, 88, 0.25), 0 0 0 1px rgba(24, 160, 88, 0.1);
|
||||||
|
background: linear-gradient(135deg, var(--card-color) 0%, rgba(24, 160, 88, 0.02) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinned-card:hover {
|
||||||
|
box-shadow: 0 8px 28px rgba(24, 160, 88, 0.35), 0 2px 12px rgba(24, 160, 88, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover-container {
|
.cover-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-container::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.1) 100%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 售罄遮罩效果 */
|
||||||
|
.goods-card:has(.tags-badge .n-tag[type="error"]) .cover-container::before {
|
||||||
|
content: '已售完';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ff5555;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 3;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pin-badge {
|
.pin-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
width: 28px;
|
width: 32px;
|
||||||
height: 28px;
|
height: 32px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: var(--error-color);
|
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-color-hover) 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.25), 0 0 0 2px rgba(255, 255, 255, 0.3);
|
||||||
color: white;
|
color: white;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
animation: pin-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pin-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: rotate(45deg) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(45deg) scale(1.05);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-badge {
|
.price-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background: linear-gradient(135deg, rgba(24, 160, 88, 0.95) 0%, rgba(18, 130, 70, 0.95) 100%);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 4px 8px;
|
padding: 6px 12px;
|
||||||
border-top-left-radius: 6px;
|
border-top-left-radius: 8px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags-badge {
|
.tags-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background: linear-gradient(135deg, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0.6) 100%);
|
||||||
padding: 4px 8px;
|
padding: 6px 8px;
|
||||||
border-top-right-radius: 6px;
|
border-top-right-radius: 8px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
max-width: 70%;
|
max-width: 65%;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-text {
|
.price-text {
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
font-size: 0.9em;
|
font-size: 1em;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-row {
|
.title-row {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 8px;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-container {
|
.title-container {
|
||||||
max-width: 70%;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-title {
|
.goods-title {
|
||||||
font-size: 1em;
|
font-size: 1.05em;
|
||||||
line-height: 1.3;
|
line-height: 1.4;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-section {
|
.content-section {
|
||||||
@@ -295,28 +376,36 @@ const emptyCover = `${IMGUR_URL}None.png`
|
|||||||
|
|
||||||
.tags-container {
|
.tags-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-height: 40px;
|
max-height: 44px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-top: 4px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags-wrapper {
|
.tags-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-tag {
|
.user-tag {
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-tag:hover {
|
.user-tag:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px) scale(1.05);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stock-info {
|
.stock-info {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
color: var(--text-color-3);
|
color: var(--text-color-3);
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background-color: var(--action-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import HyperDX from '@hyperdx/browser'
|
|
||||||
import EasySpeech from 'easy-speech'
|
import EasySpeech from 'easy-speech'
|
||||||
import { createDiscreteApi, NButton, NFlex, NText } from 'naive-ui'
|
import { createDiscreteApi, NButton, NFlex, NText } from 'naive-ui'
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
@@ -50,6 +49,8 @@ export function InitVTsuru() {
|
|||||||
|
|
||||||
async function InitOther() {
|
async function InitOther() {
|
||||||
if (process.env.NODE_ENV !== 'development' && !window.$route.path.startsWith('/obs')) {
|
if (process.env.NODE_ENV !== 'development' && !window.$route.path.startsWith('/obs')) {
|
||||||
|
const mod = await import('@hyperdx/browser')
|
||||||
|
const HyperDX = (mod as any).default ?? mod
|
||||||
HyperDX.init({
|
HyperDX.init({
|
||||||
apiKey: '7d1eb66c-24b8-445e-a406-dc2329fa9423',
|
apiKey: '7d1eb66c-24b8-445e-a406-dc2329fa9423',
|
||||||
service: 'vtsuru.live',
|
service: 'vtsuru.live',
|
||||||
@@ -58,6 +59,8 @@ async function InitOther() {
|
|||||||
advancedNetworkCapture: true, // Capture full HTTP request/response headers and bodies (default false)
|
advancedNetworkCapture: true, // Capture full HTTP request/response headers and bodies (default false)
|
||||||
ignoreUrls: [/localhost/i],
|
ignoreUrls: [/localhost/i],
|
||||||
})
|
})
|
||||||
|
// 将实例挂到窗口,便于后续设置全局属性(可选)
|
||||||
|
;(window as any).__HyperDX__ = HyperDX
|
||||||
}
|
}
|
||||||
// 加载其他数据
|
// 加载其他数据
|
||||||
InitTTS()
|
InitTTS()
|
||||||
@@ -68,7 +71,8 @@ async function InitOther() {
|
|||||||
if (account.value.biliUserAuthInfo && !useAuth.currentToken) {
|
if (account.value.biliUserAuthInfo && !useAuth.currentToken) {
|
||||||
useAuth.currentToken = account.value.biliUserAuthInfo.token
|
useAuth.currentToken = account.value.biliUserAuthInfo.token
|
||||||
}
|
}
|
||||||
HyperDX.setGlobalAttributes({
|
const HyperDX = (window as any).__HyperDX__
|
||||||
|
HyperDX?.setGlobalAttributes({
|
||||||
userId: account.value.id.toString(),
|
userId: account.value.id.toString(),
|
||||||
userName: account.value.name,
|
userName: account.value.name,
|
||||||
})
|
})
|
||||||
@@ -141,7 +145,7 @@ function InitTTS() {
|
|||||||
} else {
|
} else {
|
||||||
console.log('[SpeechSynthesis] 当前浏览器不支持tts服务')
|
console.log('[SpeechSynthesis] 当前浏览器不支持tts服务')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
console.log('[SpeechSynthesis] 当前浏览器不支持tts服务')
|
console.log('[SpeechSynthesis] 当前浏览器不支持tts服务')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { defineAsyncComponent, markRaw, ref } from 'vue'
|
import { defineAsyncComponent, markRaw, ref } from 'vue'
|
||||||
import DefaultIndexTemplateVue from '@/views/view/indexTemplate/DefaultIndexTemplate.vue'
|
|
||||||
|
|
||||||
const debugAPI
|
const debugAPI
|
||||||
= import.meta.env.VITE_API == 'dev'
|
= import.meta.env.VITE_API == 'dev'
|
||||||
@@ -126,7 +125,9 @@ export const IndexTemplateMap: TemplateMapType = {
|
|||||||
'': {
|
'': {
|
||||||
name: '默认',
|
name: '默认',
|
||||||
// settingName: 'Template.Index.Default',
|
// settingName: 'Template.Index.Default',
|
||||||
component: markRaw(DefaultIndexTemplateVue),
|
component: markRaw(defineAsyncComponent(
|
||||||
|
async () => import('@/views/view/indexTemplate/DefaultIndexTemplate.vue'),
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
src/main.ts
23
src/main.ts
@@ -1,23 +1,10 @@
|
|||||||
import { loader } from '@guolao/vue-monaco-editor'
|
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import { InitVTsuru } from './data/Initializer'
|
|
||||||
import emitter from './mitt'
|
import emitter from './mitt'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import { isTauri } from './data/constants'
|
|
||||||
import { startHeartbeat } from './client/data/initialize'
|
|
||||||
|
|
||||||
loader.config({
|
// Monaco 的 worker 在编辑器组件中懒加载配置
|
||||||
'paths': {
|
|
||||||
vs: 'https://cdn.jsdelivr.net/npm/monaco-editor/min/vs',
|
|
||||||
},
|
|
||||||
'vs/nls': {
|
|
||||||
availableLanguages: {
|
|
||||||
'*': 'zh-cn',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
export const getPinia = () => pinia
|
export const getPinia = () => pinia
|
||||||
@@ -25,9 +12,13 @@ export const getPinia = () => pinia
|
|||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(router).use(pinia).mount('#app')
|
app.use(router).use(pinia).mount('#app')
|
||||||
|
|
||||||
InitVTsuru()
|
// 将初始化逻辑改为异步按需加载,避免把其依赖打入入口
|
||||||
|
void import('./data/Initializer').then(m => m.InitVTsuru())
|
||||||
|
// 本地化 isTauri,避免在入口引入大量常量与模板映射
|
||||||
|
const isTauri = () => (window as any).__TAURI__ !== undefined || (window as any).__TAURI_INTERNAL__ !== undefined || '__TAURI__' in window
|
||||||
if (isTauri()) {
|
if (isTauri()) {
|
||||||
startHeartbeat();
|
// 仅在 Tauri 环境下才动态加载相关初始化,避免把 @tauri-apps/* 打入入口
|
||||||
|
void import('./client/data/initialize').then(m => m.startHeartbeat())
|
||||||
}
|
}
|
||||||
|
|
||||||
window.$mitt = emitter
|
window.$mitt = emitter
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DanmujiConfig } from '../obs/DanmujiOBS.vue'
|
import type { DanmujiConfig } from '../obs/DanmujiOBS.vue'
|
||||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||||
import {
|
import {
|
||||||
NButton,
|
NButton,
|
||||||
NCard,
|
NCard,
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
NTabs,
|
NTabs,
|
||||||
useMessage,
|
useMessage,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
import MonacoEditorComponent from '@/components/MonacoEditorComponent.vue'
|
||||||
import { DownloadConfig, UploadConfig, useAccount } from '@/api/account'
|
import { DownloadConfig, UploadConfig, useAccount } from '@/api/account'
|
||||||
import { EventDataTypes, GuardLevel } from '@/api/api-models'
|
import { EventDataTypes, GuardLevel } from '@/api/api-models'
|
||||||
import { CURRENT_HOST, defaultDanmujiCss } from '@/data/constants'
|
import { CURRENT_HOST, defaultDanmujiCss } from '@/data/constants'
|
||||||
@@ -61,106 +61,35 @@ const guardLevelOptions = [
|
|||||||
{ label: '总督', value: GuardLevel.Zongdu },
|
{ label: '总督', value: GuardLevel.Zongdu },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 随机弹幕内容库
|
function randomDigits(length = 4) {
|
||||||
const randomMessages = [
|
const min = length > 1 ? 10 ** (length - 1) : 0
|
||||||
'草草草草草',
|
const max = 10 ** length - 1
|
||||||
'?????',
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||||
'来了来了',
|
}
|
||||||
'呜呜呜呜呜',
|
|
||||||
'寄!',
|
|
||||||
'笑死我了',
|
|
||||||
'这也太可爱了吧!',
|
|
||||||
'awsl',
|
|
||||||
'哈↑哈↑哈↑哈↑',
|
|
||||||
'前方高能',
|
|
||||||
'妈妈爱你',
|
|
||||||
'三连了!',
|
|
||||||
'给大佬递茶',
|
|
||||||
'答应我,别鸽了',
|
|
||||||
'好耶!',
|
|
||||||
'啊这',
|
|
||||||
'我超,好听!',
|
|
||||||
'永远的神!',
|
|
||||||
'给大家笑一个',
|
|
||||||
'555555',
|
|
||||||
'鸽子本鸽',
|
|
||||||
'主播牛逼',
|
|
||||||
'下次一定充钱',
|
|
||||||
'摸摸头',
|
|
||||||
'刚来,错过了什么',
|
|
||||||
'老板大气,老板身体健康',
|
|
||||||
'皮套萌萌哒',
|
|
||||||
'这个建模好精致',
|
|
||||||
'有没有录播组',
|
|
||||||
'狗头保命',
|
|
||||||
]
|
|
||||||
|
|
||||||
// 随机用户名库
|
function generateTestUsername() {
|
||||||
const randomUsernames = [
|
return `测试用户${randomDigits(5)}`
|
||||||
'嘉晚饭',
|
}
|
||||||
'嘉心糖',
|
|
||||||
'阿梓的狗',
|
|
||||||
'向晚大魔王',
|
|
||||||
'贝极星',
|
|
||||||
'泰文一',
|
|
||||||
'一个魂们',
|
|
||||||
'琳狼粉丝',
|
|
||||||
'乃贝时光',
|
|
||||||
'呜米小籽',
|
|
||||||
'柚恩家人',
|
|
||||||
'冰糖嘎嘣脆',
|
|
||||||
'柠宝推推',
|
|
||||||
'七海Nana7mi',
|
|
||||||
'鸟P',
|
|
||||||
'珈乐厨',
|
|
||||||
'珈乐时代',
|
|
||||||
'乃琳Queen',
|
|
||||||
'贝拉kira',
|
|
||||||
'小希厨子',
|
|
||||||
'阿夸单推',
|
|
||||||
'白上单推人',
|
|
||||||
'ぺこら推し',
|
|
||||||
'星街永远爱',
|
|
||||||
'吹雪的狗',
|
|
||||||
]
|
|
||||||
|
|
||||||
// 随机礼物名库
|
function generateTestMessage() {
|
||||||
const randomGifts = [
|
const templates = [
|
||||||
'小心心',
|
'测试消息',
|
||||||
'告白气球',
|
'这是一条测试消息',
|
||||||
'打call',
|
'测试弹幕内容',
|
||||||
'奶茶',
|
'系统测试消息',
|
||||||
'小花花',
|
'模拟展示消息',
|
||||||
'小星星',
|
]
|
||||||
'蛋糕',
|
const template = templates[Math.floor(Math.random() * templates.length)]
|
||||||
'冰阔落',
|
return `${template}${randomDigits(4)}`
|
||||||
'告白气球',
|
}
|
||||||
'比心',
|
|
||||||
'小电视',
|
|
||||||
'棒棒糖',
|
|
||||||
'荧光棒',
|
|
||||||
'小黄鸭',
|
|
||||||
'小飞船',
|
|
||||||
]
|
|
||||||
|
|
||||||
// 随机粉丝牌名称库
|
function generateTestGiftName() {
|
||||||
const randomMedalNames = [
|
return `测试礼物${randomDigits(3)}`
|
||||||
'魂组',
|
}
|
||||||
'DD団',
|
|
||||||
'天狗部',
|
function generateTestMedalName() {
|
||||||
'单推人',
|
return `测试粉丝牌${randomDigits(3)}`
|
||||||
'崩坏',
|
}
|
||||||
'幸运星',
|
|
||||||
'白上组',
|
|
||||||
'星街家',
|
|
||||||
'兔田团',
|
|
||||||
'夜空社',
|
|
||||||
'天使党',
|
|
||||||
'虹团',
|
|
||||||
'杏仁',
|
|
||||||
'梦追人',
|
|
||||||
'VVota',
|
|
||||||
]
|
|
||||||
|
|
||||||
// 保存DanmujiConfig的配置
|
// 保存DanmujiConfig的配置
|
||||||
const danmujiConfig = useStorage<DanmujiConfig>('danmuji-config', {
|
const danmujiConfig = useStorage<DanmujiConfig>('danmuji-config', {
|
||||||
@@ -236,7 +165,7 @@ function resetConfigToDefault() {
|
|||||||
// 随机生成测试弹幕内容
|
// 随机生成测试弹幕内容
|
||||||
function generateRandomContent() {
|
function generateRandomContent() {
|
||||||
// 随机生成用户名
|
// 随机生成用户名
|
||||||
testFormData.uname = randomUsernames[Math.floor(Math.random() * randomUsernames.length)]
|
testFormData.uname = generateTestUsername()
|
||||||
|
|
||||||
// 随机生成用户ID (10000-99999)
|
// 随机生成用户ID (10000-99999)
|
||||||
testFormData.uid = Math.floor(Math.random() * 90000) + 10000
|
testFormData.uid = Math.floor(Math.random() * 90000) + 10000
|
||||||
@@ -245,11 +174,11 @@ function generateRandomContent() {
|
|||||||
switch (testFormData.type) {
|
switch (testFormData.type) {
|
||||||
case EventDataTypes.Message:
|
case EventDataTypes.Message:
|
||||||
// 随机弹幕内容
|
// 随机弹幕内容
|
||||||
testFormData.msg = randomMessages[Math.floor(Math.random() * randomMessages.length)]
|
testFormData.msg = generateTestMessage()
|
||||||
// 随机粉丝牌等级 (0-30)
|
// 随机粉丝牌等级 (0-30)
|
||||||
testFormData.fans_medal_level = Math.floor(Math.random() * 31)
|
testFormData.fans_medal_level = Math.floor(Math.random() * 31)
|
||||||
// 随机粉丝牌名称
|
// 随机粉丝牌名称
|
||||||
testFormData.fans_medal_name = randomMedalNames[Math.floor(Math.random() * randomMedalNames.length)]
|
testFormData.fans_medal_name = generateTestMedalName()
|
||||||
// 随机舰长等级
|
// 随机舰长等级
|
||||||
const guardRandomIndex = Math.floor(Math.random() * guardLevelOptions.length)
|
const guardRandomIndex = Math.floor(Math.random() * guardLevelOptions.length)
|
||||||
testFormData.guard_level = guardLevelOptions[guardRandomIndex].value
|
testFormData.guard_level = guardLevelOptions[guardRandomIndex].value
|
||||||
@@ -257,7 +186,7 @@ function generateRandomContent() {
|
|||||||
|
|
||||||
case EventDataTypes.Gift:
|
case EventDataTypes.Gift:
|
||||||
// 随机礼物名称
|
// 随机礼物名称
|
||||||
testFormData.msg = randomGifts[Math.floor(Math.random() * randomGifts.length)]
|
testFormData.msg = generateTestGiftName()
|
||||||
// 随机礼物数量 (1-99)
|
// 随机礼物数量 (1-99)
|
||||||
testFormData.num = Math.floor(Math.random() * 99) + 1
|
testFormData.num = Math.floor(Math.random() * 99) + 1
|
||||||
// 随机礼物价值 (1-50)
|
// 随机礼物价值 (1-50)
|
||||||
@@ -273,7 +202,7 @@ function generateRandomContent() {
|
|||||||
|
|
||||||
case EventDataTypes.SC:
|
case EventDataTypes.SC:
|
||||||
// 随机SC内容
|
// 随机SC内容
|
||||||
testFormData.msg = randomMessages[Math.floor(Math.random() * randomMessages.length)]
|
testFormData.msg = generateTestMessage()
|
||||||
// 随机SC价格 (5-500)
|
// 随机SC价格 (5-500)
|
||||||
testFormData.price = Math.floor(Math.random() * 496) + 5
|
testFormData.price = Math.floor(Math.random() * 496) + 5
|
||||||
break
|
break
|
||||||
@@ -432,7 +361,7 @@ function startAutoGenerate() {
|
|||||||
// 为自动生成弹幕生成随机内容
|
// 为自动生成弹幕生成随机内容
|
||||||
function generateAutoContent() {
|
function generateAutoContent() {
|
||||||
// 随机生成用户名
|
// 随机生成用户名
|
||||||
autoGenData.uname = randomUsernames[Math.floor(Math.random() * randomUsernames.length)]
|
autoGenData.uname = generateTestUsername()
|
||||||
|
|
||||||
// 随机生成用户ID (10000-99999)
|
// 随机生成用户ID (10000-99999)
|
||||||
autoGenData.uid = Math.floor(Math.random() * 90000) + 10000
|
autoGenData.uid = Math.floor(Math.random() * 90000) + 10000
|
||||||
@@ -441,11 +370,11 @@ function generateAutoContent() {
|
|||||||
switch (autoGenData.type) {
|
switch (autoGenData.type) {
|
||||||
case EventDataTypes.Message:
|
case EventDataTypes.Message:
|
||||||
// 随机弹幕内容
|
// 随机弹幕内容
|
||||||
autoGenData.msg = randomMessages[Math.floor(Math.random() * randomMessages.length)]
|
autoGenData.msg = generateTestMessage()
|
||||||
// 随机粉丝牌等级 (0-30)
|
// 随机粉丝牌等级 (0-30)
|
||||||
autoGenData.fans_medal_level = Math.floor(Math.random() * 31)
|
autoGenData.fans_medal_level = Math.floor(Math.random() * 31)
|
||||||
// 随机粉丝牌名称
|
// 随机粉丝牌名称
|
||||||
autoGenData.fans_medal_name = randomMedalNames[Math.floor(Math.random() * randomMedalNames.length)]
|
autoGenData.fans_medal_name = generateTestMedalName()
|
||||||
// 随机舰长等级
|
// 随机舰长等级
|
||||||
const guardRandomIndex = Math.floor(Math.random() * guardLevelOptions.length)
|
const guardRandomIndex = Math.floor(Math.random() * guardLevelOptions.length)
|
||||||
autoGenData.guard_level = guardLevelOptions[guardRandomIndex].value
|
autoGenData.guard_level = guardLevelOptions[guardRandomIndex].value
|
||||||
@@ -453,7 +382,7 @@ function generateAutoContent() {
|
|||||||
|
|
||||||
case EventDataTypes.Gift:
|
case EventDataTypes.Gift:
|
||||||
// 随机礼物名称
|
// 随机礼物名称
|
||||||
autoGenData.msg = randomGifts[Math.floor(Math.random() * randomGifts.length)]
|
autoGenData.msg = generateTestGiftName()
|
||||||
// 随机礼物数量 (1-99)
|
// 随机礼物数量 (1-99)
|
||||||
autoGenData.num = Math.floor(Math.random() * 99) + 1
|
autoGenData.num = Math.floor(Math.random() * 99) + 1
|
||||||
// 随机礼物价值 (1-50)
|
// 随机礼物价值 (1-50)
|
||||||
@@ -469,7 +398,7 @@ function generateAutoContent() {
|
|||||||
|
|
||||||
case EventDataTypes.SC:
|
case EventDataTypes.SC:
|
||||||
// 随机SC内容
|
// 随机SC内容
|
||||||
autoGenData.msg = randomMessages[Math.floor(Math.random() * randomMessages.length)]
|
autoGenData.msg = generateTestMessage()
|
||||||
// 随机SC价格 (5-500)
|
// 随机SC价格 (5-500)
|
||||||
autoGenData.price = Math.floor(Math.random() * 496) + 5
|
autoGenData.price = Math.floor(Math.random() * 496) + 5
|
||||||
break
|
break
|
||||||
@@ -663,7 +592,7 @@ async function uploadConfigToServer() {
|
|||||||
确定要重设为默认CSS吗?这将清除所有自定义样式。
|
确定要重设为默认CSS吗?这将清除所有自定义样式。
|
||||||
</NPopconfirm>
|
</NPopconfirm>
|
||||||
</template>
|
</template>
|
||||||
<VueMonacoEditor
|
<MonacoEditorComponent
|
||||||
v-model:value="css"
|
v-model:value="css"
|
||||||
language="css"
|
language="css"
|
||||||
style="height: 400px; width: 100%;"
|
style="height: 400px; width: 100%;"
|
||||||
|
|||||||
@@ -622,8 +622,8 @@ watch(
|
|||||||
:style="{ height: chartHeight }"
|
:style="{ height: chartHeight }"
|
||||||
class="chart"
|
class="chart"
|
||||||
/>
|
/>
|
||||||
|
<NDivider />
|
||||||
<NDivider>
|
<!-- <NDivider>
|
||||||
投稿播放量
|
投稿播放量
|
||||||
<NDivider vertical />
|
<NDivider vertical />
|
||||||
<NTooltip>
|
<NTooltip>
|
||||||
@@ -665,7 +665,7 @@ watch(
|
|||||||
:option="upstatLikeOption"
|
:option="upstatLikeOption"
|
||||||
:style="{ height: chartHeight }"
|
:style="{ height: chartHeight }"
|
||||||
class="chart"
|
class="chart"
|
||||||
/>
|
/> -->
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</NCard>
|
</NCard>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,7 +7,15 @@ import type {
|
|||||||
ResponsePointGoodModel,
|
ResponsePointGoodModel,
|
||||||
UploadPointGoodsModel,
|
UploadPointGoodsModel,
|
||||||
} from '@/api/api-models'
|
} from '@/api/api-models'
|
||||||
import { Info24Filled } from '@vicons/fluent'
|
import {
|
||||||
|
Add24Filled,
|
||||||
|
ArrowSync24Filled,
|
||||||
|
Delete24Filled,
|
||||||
|
Edit24Filled,
|
||||||
|
Eye24Filled,
|
||||||
|
Info24Filled,
|
||||||
|
ShoppingBag24Filled,
|
||||||
|
} from '@vicons/fluent'
|
||||||
import { useRouteHash } from '@vueuse/router'
|
import { useRouteHash } from '@vueuse/router'
|
||||||
import {
|
import {
|
||||||
NAlert,
|
NAlert,
|
||||||
@@ -517,14 +525,22 @@ onMounted(() => { })
|
|||||||
>
|
>
|
||||||
<NButton
|
<NButton
|
||||||
type="primary"
|
type="primary"
|
||||||
|
size="medium"
|
||||||
@click="onModalOpen"
|
@click="onModalOpen"
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="Add24Filled" />
|
||||||
|
</template>
|
||||||
添加礼物
|
添加礼物
|
||||||
</NButton>
|
</NButton>
|
||||||
<NButton
|
<NButton
|
||||||
secondary
|
secondary
|
||||||
|
size="medium"
|
||||||
@click="$router.push({ name: 'user-goods', params: { id: accountInfo?.name } })"
|
@click="$router.push({ name: 'user-goods', params: { id: accountInfo?.name } })"
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="Eye24Filled" />
|
||||||
|
</template>
|
||||||
前往展示页
|
前往展示页
|
||||||
</NButton>
|
</NButton>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
@@ -549,29 +565,57 @@ onMounted(() => { })
|
|||||||
class="point-goods-card"
|
class="point-goods-card"
|
||||||
>
|
>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<NFlex :gap="8">
|
<NFlex
|
||||||
|
vertical
|
||||||
|
:gap="8"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<NText style="font-size: 14px; color: var(--primary-color); font-weight: 500;">
|
||||||
|
<NIcon
|
||||||
|
:component="ShoppingBag24Filled"
|
||||||
|
style="vertical-align: -0.15em; margin-right: 4px"
|
||||||
|
/>
|
||||||
|
积分: {{ item.price }}
|
||||||
|
</NText>
|
||||||
|
<NFlex
|
||||||
|
justify="space-between"
|
||||||
|
:gap="8"
|
||||||
|
>
|
||||||
<NButton
|
<NButton
|
||||||
type="info"
|
type="info"
|
||||||
size="small"
|
size="small"
|
||||||
|
style="flex: 1"
|
||||||
@click="onUpdateClick(item)"
|
@click="onUpdateClick(item)"
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="Edit24Filled" />
|
||||||
|
</template>
|
||||||
修改
|
修改
|
||||||
</NButton>
|
</NButton>
|
||||||
<NButton
|
<NButton
|
||||||
type="warning"
|
type="warning"
|
||||||
size="small"
|
size="small"
|
||||||
|
style="flex: 1"
|
||||||
@click="onSetShelfClick(item, GoodsStatus.Discontinued)"
|
@click="onSetShelfClick(item, GoodsStatus.Discontinued)"
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="ArrowSync24Filled" />
|
||||||
|
</template>
|
||||||
下架
|
下架
|
||||||
</NButton>
|
</NButton>
|
||||||
<NButton
|
<NButton
|
||||||
type="error"
|
type="error"
|
||||||
size="small"
|
size="small"
|
||||||
|
style="flex: 1"
|
||||||
@click="onDeleteClick(item)"
|
@click="onDeleteClick(item)"
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="Delete24Filled" />
|
||||||
|
</template>
|
||||||
删除
|
删除
|
||||||
</NButton>
|
</NButton>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
|
</NFlex>
|
||||||
</template>
|
</template>
|
||||||
</PointGoodsItem>
|
</PointGoodsItem>
|
||||||
</NGridItem>
|
</NGridItem>
|
||||||
@@ -605,7 +649,13 @@ onMounted(() => { })
|
|||||||
:gap="8"
|
:gap="8"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<span>价格: {{ item.price }}</span>
|
<NText style="font-size: 14px; color: var(--primary-color); font-weight: 500;">
|
||||||
|
<NIcon
|
||||||
|
:component="ShoppingBag24Filled"
|
||||||
|
style="vertical-align: -0.15em; margin-right: 4px"
|
||||||
|
/>
|
||||||
|
积分: {{ item.price }}
|
||||||
|
</NText>
|
||||||
<NFlex
|
<NFlex
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
:gap="8"
|
:gap="8"
|
||||||
@@ -613,22 +663,34 @@ onMounted(() => { })
|
|||||||
<NButton
|
<NButton
|
||||||
type="info"
|
type="info"
|
||||||
size="small"
|
size="small"
|
||||||
|
style="flex: 1"
|
||||||
@click="onUpdateClick(item)"
|
@click="onUpdateClick(item)"
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="Edit24Filled" />
|
||||||
|
</template>
|
||||||
修改
|
修改
|
||||||
</NButton>
|
</NButton>
|
||||||
<NButton
|
<NButton
|
||||||
type="success"
|
type="success"
|
||||||
size="small"
|
size="small"
|
||||||
|
style="flex: 1"
|
||||||
@click="onSetShelfClick(item, GoodsStatus.Normal)"
|
@click="onSetShelfClick(item, GoodsStatus.Normal)"
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="ArrowSync24Filled" />
|
||||||
|
</template>
|
||||||
上架
|
上架
|
||||||
</NButton>
|
</NButton>
|
||||||
<NButton
|
<NButton
|
||||||
type="error"
|
type="error"
|
||||||
size="small"
|
size="small"
|
||||||
|
style="flex: 1"
|
||||||
@click="onDeleteClick(item)"
|
@click="onDeleteClick(item)"
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="Delete24Filled" />
|
||||||
|
</template>
|
||||||
删除
|
删除
|
||||||
</NButton>
|
</NButton>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
@@ -1151,10 +1213,18 @@ onMounted(() => { })
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-goods-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.point-goods-card :deep(.n-card-header) {
|
.point-goods-card :deep(.n-card-header) {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.point-goods-card :deep(.n-card-content) {
|
.point-goods-card :deep(.n-card-content) {
|
||||||
@@ -1163,7 +1233,9 @@ onMounted(() => { })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.point-goods-card :deep(.n-card-footer) {
|
.point-goods-card :deep(.n-card-footer) {
|
||||||
padding: 16px;
|
padding: 12px 16px;
|
||||||
|
background-color: var(--card-color);
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-modal :deep(.n-card-header) {
|
.goods-modal :deep(.n-card-header) {
|
||||||
@@ -1235,4 +1307,18 @@ onMounted(() => { })
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 按钮样式增强 */
|
||||||
|
.point-goods-card :deep(.n-button) {
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-goods-card :deep(.n-button:hover) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-goods-card :deep(.n-button:active) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -71,6 +71,20 @@ const selectedItem = ref<DataTableRowKey[]>()
|
|||||||
const targetStatus = ref<PointOrderStatus>()
|
const targetStatus = ref<PointOrderStatus>()
|
||||||
const showStatusModal = ref(false)
|
const showStatusModal = ref(false)
|
||||||
|
|
||||||
|
// 订单统计
|
||||||
|
const orderStats = computed(() => {
|
||||||
|
return {
|
||||||
|
total: orders.value.length,
|
||||||
|
pending: orders.value.filter(o => o.status === PointOrderStatus.Pending).length,
|
||||||
|
shipped: orders.value.filter(o => o.status === PointOrderStatus.Shipped).length,
|
||||||
|
completed: orders.value.filter(o => o.status === PointOrderStatus.Completed).length,
|
||||||
|
physical: orders.value.filter(o => o.type === GoodsTypes.Physical).length,
|
||||||
|
virtual: orders.value.filter(o => o.type === GoodsTypes.Virtual).length,
|
||||||
|
totalPoints: orders.value.reduce((sum, o) => sum + o.point, 0),
|
||||||
|
filteredCount: filteredOrders.value.length,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 获取所有订单
|
// 获取所有订单
|
||||||
async function getOrders() {
|
async function getOrders() {
|
||||||
try {
|
try {
|
||||||
@@ -227,10 +241,72 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<NSpin :show="isLoading">
|
<NSpin :show="isLoading">
|
||||||
<NEmpty
|
<NEmpty
|
||||||
v-if="orders.length == 0"
|
v-if="orders.length === 0"
|
||||||
description="暂无订单"
|
description="暂无订单"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<NCard
|
||||||
|
size="small"
|
||||||
|
:bordered="false"
|
||||||
|
style="margin-bottom: 16px"
|
||||||
|
>
|
||||||
|
<NFlex
|
||||||
|
justify="space-around"
|
||||||
|
wrap
|
||||||
|
:gap="16"
|
||||||
|
>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value">
|
||||||
|
{{ orderStats.total }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
总订单
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value warning">
|
||||||
|
{{ orderStats.pending }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
待发货
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value info">
|
||||||
|
{{ orderStats.shipped }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
已发货
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value success">
|
||||||
|
{{ orderStats.completed }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
已完成
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value">
|
||||||
|
{{ orderStats.physical }} / {{ orderStats.virtual }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
实体 / 虚拟
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value primary">
|
||||||
|
{{ orderStats.totalPoints }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
总积分
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NFlex>
|
||||||
|
</NCard>
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<NFlex
|
<NFlex
|
||||||
:wrap="false"
|
:wrap="false"
|
||||||
@@ -393,4 +469,52 @@ onMounted(async () => {
|
|||||||
.action-buttons {
|
.action-buttons {
|
||||||
margin: 12px 0;
|
margin: 12px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color-1);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.primary {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.success {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.info {
|
||||||
|
color: var(--info-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.warning {
|
||||||
|
color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端优化 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stat-item {
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import {
|
|||||||
NInputGroup,
|
NInputGroup,
|
||||||
NInputGroupLabel,
|
NInputGroupLabel,
|
||||||
NInputNumber,
|
NInputNumber,
|
||||||
NList,
|
|
||||||
NListItem,
|
|
||||||
NModal,
|
NModal,
|
||||||
NPopconfirm,
|
NPopconfirm,
|
||||||
NRadioButton,
|
NRadioButton,
|
||||||
@@ -470,26 +468,42 @@ async function SaveComboSetting() {
|
|||||||
justify="space-between"
|
justify="space-between"
|
||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
<span class="section-title">自定义礼物列表</span>
|
<span class="section-title">
|
||||||
|
自定义礼物列表
|
||||||
|
<NTag
|
||||||
|
v-if="Object.keys(setting.giftPercentMap).length > 0"
|
||||||
|
:bordered="false"
|
||||||
|
size="small"
|
||||||
|
type="info"
|
||||||
|
style="margin-left: 8px"
|
||||||
|
>
|
||||||
|
{{ Object.keys(setting.giftPercentMap).length }} 个礼物
|
||||||
|
</NTag>
|
||||||
|
</span>
|
||||||
<NButton
|
<NButton
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="!canEdit"
|
:disabled="!canEdit"
|
||||||
class="add-gift-button"
|
size="small"
|
||||||
@click="showAddGiftModal = true"
|
@click="showAddGiftModal = true"
|
||||||
>
|
>
|
||||||
添加礼物
|
添加礼物
|
||||||
</NButton>
|
</NButton>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
|
|
||||||
<NList bordered>
|
|
||||||
<NEmpty
|
<NEmpty
|
||||||
v-if="!Object.keys(setting.giftPercentMap).length"
|
v-if="!Object.keys(setting.giftPercentMap).length"
|
||||||
description="暂无自定义礼物"
|
description="暂无自定义礼物"
|
||||||
|
style="margin: 12px 0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NListItem
|
<div
|
||||||
|
v-else
|
||||||
|
class="gift-list"
|
||||||
|
>
|
||||||
|
<div
|
||||||
v-for="item in Object.entries(setting.giftPercentMap)"
|
v-for="item in Object.entries(setting.giftPercentMap)"
|
||||||
:key="item[0]"
|
:key="item[0]"
|
||||||
|
class="gift-item"
|
||||||
>
|
>
|
||||||
<NFlex
|
<NFlex
|
||||||
align="center"
|
align="center"
|
||||||
@@ -498,29 +512,34 @@ async function SaveComboSetting() {
|
|||||||
>
|
>
|
||||||
<NFlex
|
<NFlex
|
||||||
align="center"
|
align="center"
|
||||||
:gap="8"
|
:gap="12"
|
||||||
>
|
>
|
||||||
<NTag
|
<NTag
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
size="medium"
|
size="medium"
|
||||||
type="success"
|
type="success"
|
||||||
|
class="gift-name-tag"
|
||||||
>
|
>
|
||||||
{{ item[0] }}
|
{{ item[0] }}
|
||||||
</NTag>
|
</NTag>
|
||||||
|
<NText depth="2">
|
||||||
|
{{ setting.giftPercentMap[item[0]] }} 积分
|
||||||
|
</NText>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
|
|
||||||
<NFlex
|
<NFlex
|
||||||
align="center"
|
align="center"
|
||||||
:gap="12"
|
:gap="8"
|
||||||
>
|
>
|
||||||
<NInputGroup
|
<NInputGroup
|
||||||
style="width: 180px"
|
style="width: 140px"
|
||||||
:disabled="!canEdit"
|
:disabled="!canEdit"
|
||||||
>
|
>
|
||||||
<NInputNumber
|
<NInputNumber
|
||||||
:value="setting.giftPercentMap[item[0]]"
|
:value="setting.giftPercentMap[item[0]]"
|
||||||
:disabled="!canEdit"
|
:disabled="!canEdit"
|
||||||
min="0"
|
min="0"
|
||||||
|
size="small"
|
||||||
@update:value="(v) => (setting.giftPercentMap[item[0]] = v ? v : 0)"
|
@update:value="(v) => (setting.giftPercentMap[item[0]] = v ? v : 0)"
|
||||||
/>
|
/>
|
||||||
<NButton
|
<NButton
|
||||||
@@ -542,15 +561,14 @@ async function SaveComboSetting() {
|
|||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="Delete24Regular" />
|
<NIcon :component="Delete24Regular" />
|
||||||
</template>
|
</template>
|
||||||
删除
|
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
确定要删除这个礼物吗?
|
确定要删除这个礼物吗?
|
||||||
</NPopconfirm>
|
</NPopconfirm>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</NListItem>
|
</div>
|
||||||
</NList>
|
</div>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</NCard>
|
</NCard>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
@@ -656,6 +674,8 @@ async function SaveComboSetting() {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gift-card {
|
.gift-card {
|
||||||
@@ -663,8 +683,29 @@ async function SaveComboSetting() {
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-gift-button {
|
.gift-list {
|
||||||
max-width: 120px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-item {
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: var(--card-color);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-item:hover {
|
||||||
|
background-color: var(--hover-color);
|
||||||
|
border-color: var(--primary-color-hover);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-name-tag {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-input {
|
.modal-input {
|
||||||
@@ -691,5 +732,13 @@ async function SaveComboSetting() {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gift-item {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type {
|
|||||||
|
|
||||||
import type { ResponsePointGoodModel, ResponsePointUserModel } from '@/api/api-models'
|
import type { ResponsePointGoodModel, ResponsePointUserModel } from '@/api/api-models'
|
||||||
import { Info24Filled, Warning24Regular } from '@vicons/fluent'
|
import { Info24Filled, Warning24Regular } from '@vicons/fluent'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useDebounceFn, useStorage } from '@vueuse/core'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import {
|
import {
|
||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
NTooltip,
|
NTooltip,
|
||||||
useMessage,
|
useMessage,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { computed, h, onMounted, ref } from 'vue'
|
import { computed, h, onMounted, ref, watch } from 'vue'
|
||||||
import { useAccount } from '@/api/account'
|
import { useAccount } from '@/api/account'
|
||||||
import { QueryGetAPI } from '@/api/query'
|
import { QueryGetAPI } from '@/api/query'
|
||||||
import { POINT_API_URL } from '@/data/constants'
|
import { POINT_API_URL } from '@/data/constants'
|
||||||
@@ -80,6 +80,20 @@ const RESET_CONFIRM_TEXT = '我确认删除'
|
|||||||
|
|
||||||
// 用户数据
|
// 用户数据
|
||||||
const users = ref<ResponsePointUserModel[]>([])
|
const users = ref<ResponsePointUserModel[]>([])
|
||||||
|
|
||||||
|
// 搜索关键词
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const debouncedSearchKeyword = ref('')
|
||||||
|
|
||||||
|
// 防抖搜索
|
||||||
|
const updateSearch = useDebounceFn((value: string) => {
|
||||||
|
debouncedSearchKeyword.value = value
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
watch(searchKeyword, (newVal) => {
|
||||||
|
updateSearch(newVal)
|
||||||
|
})
|
||||||
|
|
||||||
// 根据筛选条件过滤后的用户
|
// 根据筛选条件过滤后的用户
|
||||||
const filteredUsers = computed(() => {
|
const filteredUsers = computed(() => {
|
||||||
return users.value
|
return users.value
|
||||||
@@ -90,11 +104,11 @@ const filteredUsers = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 根据关键词搜索
|
// 根据关键词搜索
|
||||||
if (settings.value.searchKeyword) {
|
if (debouncedSearchKeyword.value) {
|
||||||
const keyword = settings.value.searchKeyword.toLowerCase()
|
const keyword = debouncedSearchKeyword.value.toLowerCase()
|
||||||
return (
|
return (
|
||||||
user.info.name?.toLowerCase().includes(keyword) == true
|
user.info.name?.toLowerCase().includes(keyword) === true
|
||||||
|| user.info.userId?.toString() == keyword
|
|| user.info.userId?.toString() === keyword
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +117,18 @@ const filteredUsers = computed(() => {
|
|||||||
.sort((a, b) => b.updateAt - a.updateAt) // 按更新时间降序排序
|
.sort((a, b) => b.updateAt - a.updateAt) // 按更新时间降序排序
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 用户统计
|
||||||
|
const userStats = computed(() => {
|
||||||
|
return {
|
||||||
|
total: users.value.length,
|
||||||
|
authed: users.value.filter(u => u.isAuthed).length,
|
||||||
|
totalPoints: users.value.reduce((sum, u) => sum + u.point, 0),
|
||||||
|
totalOrders: users.value.reduce((sum, u) => sum + (u.orderCount || 0), 0),
|
||||||
|
avgPoints: users.value.length > 0 ? Math.round(users.value.reduce((sum, u) => sum + u.point, 0) / users.value.length) : 0,
|
||||||
|
filtered: filteredUsers.value.length,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 当前查看的用户详情
|
// 当前查看的用户详情
|
||||||
const currentUser = ref<ResponsePointUserModel>()
|
const currentUser = ref<ResponsePointUserModel>()
|
||||||
|
|
||||||
@@ -383,6 +409,60 @@ onMounted(async () => {
|
|||||||
:show="isLoading"
|
:show="isLoading"
|
||||||
class="user-manage-container"
|
class="user-manage-container"
|
||||||
>
|
>
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<NCard
|
||||||
|
size="small"
|
||||||
|
:bordered="false"
|
||||||
|
style="margin-bottom: 16px"
|
||||||
|
>
|
||||||
|
<NFlex
|
||||||
|
justify="space-around"
|
||||||
|
wrap
|
||||||
|
:gap="16"
|
||||||
|
>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value">
|
||||||
|
{{ userStats.total }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
总用户
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value success">
|
||||||
|
{{ userStats.authed }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
已认证
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value primary">
|
||||||
|
{{ userStats.totalPoints }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
总积分
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value info">
|
||||||
|
{{ userStats.totalOrders }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
总订单
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value">
|
||||||
|
{{ userStats.avgPoints }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
平均积分
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NFlex>
|
||||||
|
</NCard>
|
||||||
|
|
||||||
<!-- 设置卡片 -->
|
<!-- 设置卡片 -->
|
||||||
<NCard title="设置">
|
<NCard title="设置">
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
@@ -442,11 +522,16 @@ onMounted(async () => {
|
|||||||
:gap="5"
|
:gap="5"
|
||||||
>
|
>
|
||||||
<NInput
|
<NInput
|
||||||
v-model:value="settings.searchKeyword"
|
v-model:value="searchKeyword"
|
||||||
placeholder="搜索用户 (用户名或UID)"
|
placeholder="搜索用户 (用户名或UID)"
|
||||||
style="max-width: 200px"
|
style="width: 220px"
|
||||||
clearable
|
clearable
|
||||||
/>
|
size="small"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
🔍
|
||||||
|
</template>
|
||||||
|
</NInput>
|
||||||
<NTooltip>
|
<NTooltip>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NIcon :component="Info24Filled" />
|
<NIcon :component="Info24Filled" />
|
||||||
@@ -638,6 +723,35 @@ onMounted(async () => {
|
|||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color-1);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.primary {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.success {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.info {
|
||||||
|
color: var(--info-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color-3);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.table-actions {
|
.table-actions {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -647,5 +761,13 @@ onMounted(async () => {
|
|||||||
.table-actions > * {
|
.table-actions > * {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { useDebounceFn } from '@vueuse/core'
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
import _ from 'lodash'
|
import { cloneDeep } from 'lodash-es'
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import * as constants from './constants'
|
import * as constants from './constants'
|
||||||
import MembershipItem from './MembershipItem.vue'
|
import MembershipItem from './MembershipItem.vue'
|
||||||
@@ -51,7 +51,7 @@ export default defineComponent({
|
|||||||
const customStyleElement = document.createElement('style')
|
const customStyleElement = document.createElement('style')
|
||||||
document.head.appendChild(customStyleElement)
|
document.head.appendChild(customStyleElement)
|
||||||
const setCssDebounce = useDebounceFn(() => {
|
const setCssDebounce = useDebounceFn(() => {
|
||||||
customStyleElement.innerHTML = this.customCss ?? ''
|
customStyleElement.innerHTML = this.customCss || ''
|
||||||
console.log('[blivechat] 已设置自定义样式')
|
console.log('[blivechat] 已设置自定义样式')
|
||||||
}, 1000)
|
}, 1000)
|
||||||
return {
|
return {
|
||||||
@@ -96,10 +96,10 @@ export default defineComponent({
|
|||||||
canScrollToBottom(val) {
|
canScrollToBottom(val) {
|
||||||
this.cantScrollStartTime = val ? null : new Date()
|
this.cantScrollStartTime = val ? null : new Date()
|
||||||
},
|
},
|
||||||
watchCustomCss: {
|
customCss: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(val, oldVal) {
|
handler() {
|
||||||
this.setCssDebounce(val)
|
this.setCssDebounce()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -260,7 +260,7 @@ export default defineComponent({
|
|||||||
this.enqueueIntervals.splice(0, this.enqueueIntervals.length - 5)
|
this.enqueueIntervals.splice(0, this.enqueueIntervals.length - 5)
|
||||||
}
|
}
|
||||||
// 这边估计得尽量大,只要不太早把消息缓冲发完就是平滑的。有MESSAGE_MAX_INTERVAL保底,不会让消息延迟太大
|
// 这边估计得尽量大,只要不太早把消息缓冲发完就是平滑的。有MESSAGE_MAX_INTERVAL保底,不会让消息延迟太大
|
||||||
// 其实可以用单调队列求最大值,偷懒不写了
|
// 使用Math.max计算最大值来估计下次入队时间间隔
|
||||||
this.estimatedEnqueueInterval = Math.max(...this.enqueueIntervals)
|
this.estimatedEnqueueInterval = Math.max(...this.enqueueIntervals)
|
||||||
}
|
}
|
||||||
// 上次入队时间还是要设置,否则会太早把消息缓冲发完,然后较长时间没有新消息
|
// 上次入队时间还是要设置,否则会太早把消息缓冲发完,然后较长时间没有新消息
|
||||||
@@ -280,10 +280,8 @@ export default defineComponent({
|
|||||||
if (messageGroup.length > 0) {
|
if (messageGroup.length > 0) {
|
||||||
if (this.smoothedMessageQueue.length > 0) {
|
if (this.smoothedMessageQueue.length > 0) {
|
||||||
// 和上一组合并
|
// 和上一组合并
|
||||||
const lastMessageGroup = this.smoothedMessageQueue[this.smoothedMessageQueue.length - 1]
|
const lastMessageGroup = this.smoothedMessageQueue.at(-1)
|
||||||
for (const message of messageGroup) {
|
lastMessageGroup.push(...messageGroup)
|
||||||
lastMessageGroup.push(message)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 自己一个组
|
// 自己一个组
|
||||||
this.smoothedMessageQueue.push(messageGroup)
|
this.smoothedMessageQueue.push(messageGroup)
|
||||||
@@ -325,12 +323,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
// 发消息
|
// 发消息
|
||||||
const messageGroups = this.smoothedMessageQueue.splice(0, groupNumToEmit)
|
const messageGroups = this.smoothedMessageQueue.splice(0, groupNumToEmit)
|
||||||
const mergedGroup = []
|
const mergedGroup = messageGroups.flat()
|
||||||
for (const messageGroup of messageGroups) {
|
|
||||||
for (const message of messageGroup) {
|
|
||||||
mergedGroup.push(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.handleMessageGroup(mergedGroup)
|
this.handleMessageGroup(mergedGroup)
|
||||||
|
|
||||||
if (this.smoothedMessageQueue.length <= 0) {
|
if (this.smoothedMessageQueue.length <= 0) {
|
||||||
@@ -386,7 +379,7 @@ export default defineComponent({
|
|||||||
message.addTime = new Date()
|
message.addTime = new Date()
|
||||||
|
|
||||||
if (message.type !== constants.MESSAGE_TYPE_TEXT) {
|
if (message.type !== constants.MESSAGE_TYPE_TEXT) {
|
||||||
this.paidMessages.unshift(_.cloneDeep(message))
|
this.paidMessages.unshift(cloneDeep(message))
|
||||||
const MAX_PAID_MESSAGE_NUM = 100
|
const MAX_PAID_MESSAGE_NUM = 100
|
||||||
if (this.paidMessages.length > MAX_PAID_MESSAGE_NUM) {
|
if (this.paidMessages.length > MAX_PAID_MESSAGE_NUM) {
|
||||||
this.paidMessages.splice(MAX_PAID_MESSAGE_NUM, this.paidMessages.length - MAX_PAID_MESSAGE_NUM)
|
this.paidMessages.splice(MAX_PAID_MESSAGE_NUM, this.paidMessages.length - MAX_PAID_MESSAGE_NUM)
|
||||||
|
|||||||
@@ -181,7 +181,15 @@ export function getShowRichContent(message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getShowContentParts(message) {
|
export function getShowContentParts(message) {
|
||||||
const contentParts = [...message.contentParts || []]
|
const contentParts = Array.isArray(message.contentParts)
|
||||||
|
? [...message.contentParts]
|
||||||
|
: []
|
||||||
|
if (contentParts.length === 0 && message.content) {
|
||||||
|
contentParts.push({
|
||||||
|
type: CONTENT_TYPE_TEXT,
|
||||||
|
text: message.content,
|
||||||
|
})
|
||||||
|
}
|
||||||
if (message.translation) {
|
if (message.translation) {
|
||||||
contentParts.push({
|
contentParts.push({
|
||||||
type: CONTENT_TYPE_TEXT,
|
type: CONTENT_TYPE_TEXT,
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ import {
|
|||||||
NText,
|
NText,
|
||||||
NTree,
|
NTree,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { computed, h, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, defineAsyncComponent, h, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { controllerBodies, controllerStructures, gamepadConfigs } from '@/data/gamepadConfigs'
|
import { controllerBodies, controllerStructures, gamepadConfigs } from '@/data/gamepadConfigs'
|
||||||
import { useGamepadStore } from '@/store/useGamepadStore'
|
import { useGamepadStore } from '@/store/useGamepadStore'
|
||||||
import GamepadDisplay from './GamepadDisplay.vue'
|
const GamepadDisplay = defineAsyncComponent(() => import('./GamepadDisplay.vue'))
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
viewBox?: string
|
viewBox?: string
|
||||||
@@ -90,6 +90,8 @@ const bodyOptions = computed(() => availableBodies.value.map(body => ({
|
|||||||
|
|
||||||
const bodyIdStorageKey = computed(() => `gamepad-body-${selectedType.value}`)
|
const bodyIdStorageKey = computed(() => `gamepad-body-${selectedType.value}`)
|
||||||
const selectedBodyId = useStorage<string>(bodyIdStorageKey, '')
|
const selectedBodyId = useStorage<string>(bodyIdStorageKey, '')
|
||||||
|
// 是否显示实时预览(避免首次进入即加载大体积渲染资源)
|
||||||
|
const showPreview = ref(false)
|
||||||
|
|
||||||
// 当手柄类型变化时重置相关配置
|
// 当手柄类型变化时重置相关配置
|
||||||
watch(selectedType, () => {
|
watch(selectedType, () => {
|
||||||
@@ -423,7 +425,12 @@ const gamepadDisplayUrl = computed(() => {
|
|||||||
size="small"
|
size="small"
|
||||||
style="margin-top: 10px;"
|
style="margin-top: 10px;"
|
||||||
>
|
>
|
||||||
<div style="position: relative; width: 100%; height: 300px; background-color: #333; border-radius: 8px; overflow: hidden;">
|
<NSpace align="center" size="small" style="margin-bottom: 6px;">
|
||||||
|
<NButton size="small" type="info" @click="showPreview = !showPreview">
|
||||||
|
{{ showPreview ? '隐藏预览' : '显示预览' }}
|
||||||
|
</NButton>
|
||||||
|
</NSpace>
|
||||||
|
<div v-if="showPreview" style="position: relative; width: 100%; height: 300px; background-color: #333; border-radius: 8px; overflow: hidden;">
|
||||||
<GamepadDisplay
|
<GamepadDisplay
|
||||||
:key="selectedType"
|
:key="selectedType"
|
||||||
:type="selectedType"
|
:type="selectedType"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
ResponsePointOrder2UserModel,
|
ResponsePointOrder2UserModel,
|
||||||
UserInfo,
|
UserInfo,
|
||||||
} from '@/api/api-models'
|
} from '@/api/api-models'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
import {
|
import {
|
||||||
NAlert,
|
NAlert,
|
||||||
NButton,
|
NButton,
|
||||||
@@ -31,7 +32,7 @@ import {
|
|||||||
useDialog,
|
useDialog,
|
||||||
useMessage,
|
useMessage,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { computed, h, onMounted, ref } from 'vue'
|
import { computed, h, onMounted, ref, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
GoodsTypes,
|
GoodsTypes,
|
||||||
@@ -73,6 +74,16 @@ const onlyCanBuy = ref(false) // 只显示可兑换
|
|||||||
const ignoreGuard = ref(false) // 忽略舰长限制
|
const ignoreGuard = ref(false) // 忽略舰长限制
|
||||||
const sortOrder = ref<string | null>(null) // 排序方式
|
const sortOrder = ref<string | null>(null) // 排序方式
|
||||||
const searchKeyword = ref('') // 搜索关键词
|
const searchKeyword = ref('') // 搜索关键词
|
||||||
|
const debouncedSearchKeyword = ref('') // 防抖后的搜索关键词
|
||||||
|
|
||||||
|
// 防抖搜索
|
||||||
|
const updateSearch = useDebounceFn((value: string) => {
|
||||||
|
debouncedSearchKeyword.value = value
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
watch(searchKeyword, (newVal) => {
|
||||||
|
updateSearch(newVal)
|
||||||
|
})
|
||||||
|
|
||||||
// --- 计算属性 ---
|
// --- 计算属性 ---
|
||||||
|
|
||||||
@@ -117,9 +128,9 @@ const selectedItems = computed(() => {
|
|||||||
// 关键词搜索 (匹配名称或描述)
|
// 关键词搜索 (匹配名称或描述)
|
||||||
.filter(
|
.filter(
|
||||||
item =>
|
item =>
|
||||||
!searchKeyword.value
|
!debouncedSearchKeyword.value
|
||||||
|| item.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
|| item.name.toLowerCase().includes(debouncedSearchKeyword.value.toLowerCase())
|
||||||
|| (item.description && item.description.toLowerCase().includes(searchKeyword.value.toLowerCase())),
|
|| (item.description && item.description.toLowerCase().includes(debouncedSearchKeyword.value.toLowerCase())),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 应用排序方式
|
// 应用排序方式
|
||||||
@@ -163,12 +174,6 @@ const selectedItems = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取商品标签颜色
|
|
||||||
function getTagColor(index: number): 'default' | 'info' | 'success' | 'warning' | 'error' | 'primary' {
|
|
||||||
const colors: Array<'default' | 'info' | 'success' | 'warning' | 'error' | 'primary'> = ['default', 'info', 'success', 'warning', 'error']
|
|
||||||
return colors[index % colors.length]
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 方法 ---
|
// --- 方法 ---
|
||||||
|
|
||||||
// 获取礼物兑换按钮的提示文本
|
// 获取礼物兑换按钮的提示文本
|
||||||
@@ -746,7 +751,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filter-section {
|
.filter-section {
|
||||||
padding: 12px 16px;
|
padding: 10px;
|
||||||
background-color: var(--action-color);
|
background-color: var(--action-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -777,22 +782,25 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.search-filter-row {
|
.search-filter-row {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
max-width: 200px;
|
min-width: 180px;
|
||||||
|
max-width: 250px;
|
||||||
|
flex: 1 1 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-options {
|
.filter-options {
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-checkbox {
|
.filter-checkbox {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.sort-select {
|
white-space: nowrap;
|
||||||
width: 120px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-list-container {
|
.goods-list-container {
|
||||||
@@ -807,25 +815,64 @@ onMounted(async () => {
|
|||||||
.goods-item {
|
.goods-item {
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
background-color: var(--card-color);
|
background-color: var(--card-color);
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-item:hover {
|
.goods-item::before {
|
||||||
transform: translateY(-3px);
|
content: '';
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
|
||||||
|
transition: left 0.6s ease;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-item:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-item:hover {
|
||||||
|
transform: translateY(-8px) scale(1.02);
|
||||||
|
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
z-index: 2;
|
||||||
|
border-color: var(--primary-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-item:active {
|
||||||
|
transform: translateY(-6px) scale(1.01);
|
||||||
|
transition: all 0.1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pinned-item {
|
.pinned-item {
|
||||||
border: 2px solid var(--primary-color);
|
border: 2px solid var(--primary-color);
|
||||||
box-shadow: 0 2px 12px rgba(var(--primary-color-rgb), 0.15);
|
box-shadow: 0 4px 20px rgba(24, 160, 88, 0.25), 0 0 0 1px rgba(24, 160, 88, 0.1);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background: linear-gradient(135deg, var(--card-color) 0%, rgba(24, 160, 88, 0.04) 100%);
|
||||||
|
animation: subtle-glow 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinned-item:hover {
|
||||||
|
box-shadow: 0 12px 32px rgba(24, 160, 88, 0.35), 0 4px 16px rgba(24, 160, 88, 0.2);
|
||||||
|
border-color: var(--primary-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes subtle-glow {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 4px 20px rgba(24, 160, 88, 0.25), 0 0 0 1px rgba(24, 160, 88, 0.1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 6px 24px rgba(24, 160, 88, 0.35), 0 0 0 2px rgba(24, 160, 88, 0.15);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pinned-item::before {
|
.pinned-item::before {
|
||||||
@@ -897,16 +944,29 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.goods-footer {
|
.goods-footer {
|
||||||
padding: 8px;
|
padding: 10px 12px;
|
||||||
border-top: 1px solid var(--border-color-1);
|
border-top: 1px solid var(--border-color);
|
||||||
background-color: rgba(var(--card-color-rgb), 0.7);
|
background: linear-gradient(to bottom, rgba(var(--card-color-rgb), 0.5), var(--card-color));
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-btn {
|
.exchange-btn {
|
||||||
min-width: 80px;
|
min-width: 90px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 6px rgba(24, 160, 88, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-btn:not(:disabled):hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(24, 160, 88, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-btn:not(:disabled):active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 6px rgba(24, 160, 88, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-btn::after {
|
.exchange-btn::after {
|
||||||
@@ -919,7 +979,7 @@ onMounted(async () => {
|
|||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
transparent,
|
transparent,
|
||||||
rgba(255, 255, 255, 0.2),
|
rgba(255, 255, 255, 0.3),
|
||||||
transparent
|
transparent
|
||||||
);
|
);
|
||||||
transition: all 0.6s ease;
|
transition: all 0.6s ease;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ResponsePointOrder2UserModel } from '@/api/api-models'
|
import type { ResponsePointOrder2UserModel } from '@/api/api-models'
|
||||||
import { NButton, NEmpty, NFlex, NSpin, useMessage } from 'naive-ui'
|
import { NButton, NCard, NDataTable, NEmpty, NFlex, NSelect, NSpin, NTag, useMessage } from 'naive-ui'
|
||||||
import { onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { PointOrderStatus } from '@/api/api-models'
|
||||||
import PointOrderCard from '@/components/manage/PointOrderCard.vue'
|
import PointOrderCard from '@/components/manage/PointOrderCard.vue'
|
||||||
import { POINT_API_URL } from '@/data/constants'
|
import { POINT_API_URL } from '@/data/constants'
|
||||||
import { useBiliAuth } from '@/store/useBiliAuth'
|
import { useBiliAuth } from '@/store/useBiliAuth'
|
||||||
@@ -14,6 +15,42 @@ const useAuth = useBiliAuth()
|
|||||||
const orders = ref<ResponsePointOrder2UserModel[]>([])
|
const orders = ref<ResponsePointOrder2UserModel[]>([])
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
// 筛选状态
|
||||||
|
const statusFilter = ref<PointOrderStatus | null>(null)
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
|
||||||
|
// 筛选后的订单
|
||||||
|
const filteredOrders = computed(() => {
|
||||||
|
let result = orders.value
|
||||||
|
|
||||||
|
// 状态筛选
|
||||||
|
if (statusFilter.value !== null) {
|
||||||
|
result = result.filter(order => order.status === statusFilter.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索关键词筛选
|
||||||
|
if (searchKeyword.value) {
|
||||||
|
const keyword = searchKeyword.value.toLowerCase()
|
||||||
|
result = result.filter(order =>
|
||||||
|
order.goods?.name.toLowerCase().includes(keyword)
|
||||||
|
|| order.id.toString().includes(keyword),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
// 订单统计
|
||||||
|
const orderStats = computed(() => {
|
||||||
|
return {
|
||||||
|
total: orders.value.length,
|
||||||
|
pending: orders.value.filter(o => o.status === PointOrderStatus.Pending).length,
|
||||||
|
shipped: orders.value.filter(o => o.status === PointOrderStatus.Shipped).length,
|
||||||
|
completed: orders.value.filter(o => o.status === PointOrderStatus.Completed).length,
|
||||||
|
totalPoints: orders.value.reduce((sum, o) => sum + o.point, 0),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function getOrders() {
|
async function getOrders() {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
@@ -53,20 +90,137 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NSpin :show="isLoading">
|
<NSpin :show="isLoading">
|
||||||
<NFlex justify="end" style="margin-bottom: 10px">
|
<!-- 统计卡片 -->
|
||||||
<NButton size="small" type="primary" @click="getOrders">
|
<NCard
|
||||||
|
size="small"
|
||||||
|
:bordered="false"
|
||||||
|
style="margin-bottom: 16px"
|
||||||
|
>
|
||||||
|
<NFlex
|
||||||
|
justify="space-around"
|
||||||
|
wrap
|
||||||
|
:gap="16"
|
||||||
|
>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value">
|
||||||
|
{{ orderStats.total }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
总订单
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value warning">
|
||||||
|
{{ orderStats.pending }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
待发货
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value info">
|
||||||
|
{{ orderStats.shipped }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
已发货
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value success">
|
||||||
|
{{ orderStats.completed }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
已完成
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value primary">
|
||||||
|
{{ orderStats.totalPoints }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
总积分
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NFlex>
|
||||||
|
</NCard>
|
||||||
|
|
||||||
|
<!-- 筛选和搜索 -->
|
||||||
|
<NFlex
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
style="margin-bottom: 12px"
|
||||||
|
wrap
|
||||||
|
:gap="12"
|
||||||
|
>
|
||||||
|
<NFlex
|
||||||
|
:gap="12"
|
||||||
|
wrap
|
||||||
|
>
|
||||||
|
<NSelect
|
||||||
|
v-model:value="statusFilter"
|
||||||
|
:options="[
|
||||||
|
{ label: '全部状态', value: null as any },
|
||||||
|
{ label: '待发货', value: PointOrderStatus.Pending },
|
||||||
|
{ label: '已发货', value: PointOrderStatus.Shipped },
|
||||||
|
{ label: '已完成', value: PointOrderStatus.Completed },
|
||||||
|
]"
|
||||||
|
style="width: 120px"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</NFlex>
|
||||||
|
<NButton
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
@click="getOrders"
|
||||||
|
>
|
||||||
刷新订单
|
刷新订单
|
||||||
</NButton>
|
</NButton>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
|
|
||||||
<NEmpty
|
<NEmpty
|
||||||
v-if="orders.length == 0"
|
v-if="filteredOrders.length === 0"
|
||||||
description="暂无订单"
|
description="暂无订单"
|
||||||
/>
|
/>
|
||||||
<PointOrderCard
|
<PointOrderCard
|
||||||
v-else
|
v-else
|
||||||
:order="orders"
|
:order="filteredOrders"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
type="user"
|
type="user"
|
||||||
/>
|
/>
|
||||||
</NSpin>
|
</NSpin>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color-1);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.primary {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.success {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.info {
|
||||||
|
color: var(--info-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.warning {
|
||||||
|
color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color-3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ResponsePointHisrotyModel } from '@/api/api-models'
|
import type { ResponsePointHisrotyModel } from '@/api/api-models'
|
||||||
import { NButton, NEmpty, NFlex, NSelect, NSpin, useMessage } from 'naive-ui'
|
import { format } from 'date-fns'
|
||||||
|
import { saveAs } from 'file-saver'
|
||||||
|
import { NButton, NCard, NDatePicker, NEmpty, NFlex, NRadioButton, NRadioGroup, NSelect, NSpin, NStatistic, useMessage } from 'naive-ui'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { PointFrom } from '@/api/api-models'
|
import { PointFrom } from '@/api/api-models'
|
||||||
import PointHistoryCard from '@/components/manage/PointHistoryCard.vue'
|
import PointHistoryCard from '@/components/manage/PointHistoryCard.vue'
|
||||||
import { POINT_API_URL } from '@/data/constants'
|
import { POINT_API_URL } from '@/data/constants'
|
||||||
import { useBiliAuth } from '@/store/useBiliAuth'
|
import { useBiliAuth } from '@/store/useBiliAuth'
|
||||||
|
import { objectsToCSV } from '@/Utils'
|
||||||
|
|
||||||
// 定义加载完成的事件
|
// 定义加载完成的事件
|
||||||
const emit = defineEmits(['dataLoaded'])
|
const emit = defineEmits(['dataLoaded'])
|
||||||
@@ -15,6 +18,21 @@ const isLoading = ref(false)
|
|||||||
|
|
||||||
const history = ref<ResponsePointHisrotyModel[]>([])
|
const history = ref<ResponsePointHisrotyModel[]>([])
|
||||||
const streamerFilter = ref<string | null>('')
|
const streamerFilter = ref<string | null>('')
|
||||||
|
const pointTypeFilter = ref<'all' | 'increase' | 'decrease'>('all')
|
||||||
|
const dateRange = ref<[number, number] | null>(null)
|
||||||
|
|
||||||
|
// 积分历史统计
|
||||||
|
const historyStats = computed(() => {
|
||||||
|
return {
|
||||||
|
total: history.value.length,
|
||||||
|
totalIncrease: history.value.filter(h => h.point > 0).reduce((sum, h) => sum + h.point, 0),
|
||||||
|
totalDecrease: Math.abs(history.value.filter(h => h.point < 0).reduce((sum, h) => sum + h.point, 0)),
|
||||||
|
fromManual: history.value.filter(h => h.from === PointFrom.Manual).length,
|
||||||
|
fromDanmaku: history.value.filter(h => h.from === PointFrom.Danmaku).length,
|
||||||
|
fromCheckIn: history.value.filter(h => h.from === PointFrom.CheckIn).length,
|
||||||
|
fromUse: history.value.filter(h => h.from === PointFrom.Use).length,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 获取积分历史记录
|
// 获取积分历史记录
|
||||||
async function getHistories() {
|
async function getHistories() {
|
||||||
@@ -58,10 +76,11 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// 根据主播名称筛选历史记录
|
// 根据主播名称筛选历史记录
|
||||||
const filteredHistory = computed(() => {
|
const filteredHistory = computed(() => {
|
||||||
if (streamerFilter.value === '' || streamerFilter.value === null) {
|
let result = history.value
|
||||||
return history.value
|
|
||||||
}
|
// 主播筛选
|
||||||
return history.value.filter((item) => {
|
if (streamerFilter.value && streamerFilter.value !== '') {
|
||||||
|
result = result.filter((item) => {
|
||||||
// 只筛选主播操作、弹幕来源和签到
|
// 只筛选主播操作、弹幕来源和签到
|
||||||
if ([PointFrom.Manual, PointFrom.Danmaku, PointFrom.CheckIn].includes(item.from)) {
|
if ([PointFrom.Manual, PointFrom.Danmaku, PointFrom.CheckIn].includes(item.from)) {
|
||||||
// 精确匹配主播名称
|
// 精确匹配主播名称
|
||||||
@@ -74,6 +93,21 @@ const filteredHistory = computed(() => {
|
|||||||
// 其他类型的记录,在筛选时隐藏
|
// 其他类型的记录,在筛选时隐藏
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 积分类型筛选
|
||||||
|
if (pointTypeFilter.value === 'increase') {
|
||||||
|
result = result.filter(h => h.point > 0)
|
||||||
|
} else if (pointTypeFilter.value === 'decrease') {
|
||||||
|
result = result.filter(h => h.point < 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间范围筛选
|
||||||
|
if (dateRange.value && dateRange.value[0] && dateRange.value[1]) {
|
||||||
|
result = result.filter(h => h.createAt >= dateRange.value![0] && h.createAt <= dateRange.value![1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算可选的主播列表
|
// 计算可选的主播列表
|
||||||
@@ -91,14 +125,107 @@ const streamerOptions = computed(() => {
|
|||||||
})
|
})
|
||||||
return options
|
return options
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 导出积分历史数据
|
||||||
|
function exportHistoryData() {
|
||||||
|
try {
|
||||||
|
const pointFromText = {
|
||||||
|
[PointFrom.Manual]: '手动调整',
|
||||||
|
[PointFrom.Danmaku]: '弹幕',
|
||||||
|
[PointFrom.CheckIn]: '签到',
|
||||||
|
[PointFrom.Use]: '使用积分',
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = objectsToCSV(
|
||||||
|
filteredHistory.value.map((item) => {
|
||||||
|
return {
|
||||||
|
时间: format(item.createAt, 'yyyy-MM-dd HH:mm:ss'),
|
||||||
|
积分变化: item.point,
|
||||||
|
来源: pointFromText[item.from] || '未知',
|
||||||
|
主播: item.extra?.user?.name || '-',
|
||||||
|
数量: item.count || '-',
|
||||||
|
备注: item.extra?.reason || '-',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 添加BOM标记,确保Excel正确识别UTF-8编码
|
||||||
|
const BOM = new Uint8Array([0xEF, 0xBB, 0xBF])
|
||||||
|
const utf8encoder = new TextEncoder()
|
||||||
|
const utf8array = utf8encoder.encode(text)
|
||||||
|
|
||||||
|
saveAs(
|
||||||
|
new Blob([BOM, utf8array], { type: 'text/csv;charset=utf-8;' }),
|
||||||
|
`积分历史_${format(Date.now(), 'yyyy-MM-dd_HH-mm-ss')}.csv`,
|
||||||
|
)
|
||||||
|
|
||||||
|
message.success('导出成功')
|
||||||
|
} catch (error) {
|
||||||
|
message.error(`导出失败: ${error}`)
|
||||||
|
console.error('导出失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NSpin :show="isLoading">
|
<NSpin :show="isLoading">
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<NCard
|
||||||
|
size="small"
|
||||||
|
:bordered="false"
|
||||||
|
style="margin-bottom: 16px"
|
||||||
|
>
|
||||||
<NFlex
|
<NFlex
|
||||||
justify="end"
|
justify="space-around"
|
||||||
|
wrap
|
||||||
|
:gap="16"
|
||||||
|
>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value">
|
||||||
|
{{ historyStats.total }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
总记录
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value success">
|
||||||
|
+{{ historyStats.totalIncrease }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
总获得
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value error">
|
||||||
|
-{{ historyStats.totalDecrease }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
总消耗
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value primary">
|
||||||
|
{{ historyStats.totalIncrease - historyStats.totalDecrease }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">
|
||||||
|
净增加
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NFlex>
|
||||||
|
</NCard>
|
||||||
|
|
||||||
|
<!-- 筛选和搜索 -->
|
||||||
|
<NFlex
|
||||||
|
justify="space-between"
|
||||||
align="center"
|
align="center"
|
||||||
style="margin-bottom: 10px"
|
style="margin-bottom: 12px"
|
||||||
|
wrap
|
||||||
|
:gap="12"
|
||||||
|
>
|
||||||
|
<NFlex
|
||||||
|
:gap="12"
|
||||||
|
wrap
|
||||||
>
|
>
|
||||||
<NSelect
|
<NSelect
|
||||||
v-model:value="streamerFilter"
|
v-model:value="streamerFilter"
|
||||||
@@ -106,8 +233,40 @@ const streamerOptions = computed(() => {
|
|||||||
placeholder="按主播筛选"
|
placeholder="按主播筛选"
|
||||||
clearable
|
clearable
|
||||||
size="small"
|
size="small"
|
||||||
style="max-width: 200px; margin-right: 10px"
|
style="min-width: 120px; max-width: 180px"
|
||||||
/>
|
/>
|
||||||
|
<NRadioGroup
|
||||||
|
v-model:value="pointTypeFilter"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<NRadioButton value="all">
|
||||||
|
全部
|
||||||
|
</NRadioButton>
|
||||||
|
<NRadioButton value="increase">
|
||||||
|
增加
|
||||||
|
</NRadioButton>
|
||||||
|
<NRadioButton value="decrease">
|
||||||
|
减少
|
||||||
|
</NRadioButton>
|
||||||
|
</NRadioGroup>
|
||||||
|
<NDatePicker
|
||||||
|
v-model:value="dateRange"
|
||||||
|
type="datetimerange"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
style="max-width: 360px"
|
||||||
|
placeholder="选择时间范围"
|
||||||
|
/>
|
||||||
|
</NFlex>
|
||||||
|
<NFlex :gap="8">
|
||||||
|
<NButton
|
||||||
|
size="small"
|
||||||
|
type="info"
|
||||||
|
secondary
|
||||||
|
@click="exportHistoryData"
|
||||||
|
>
|
||||||
|
导出数据
|
||||||
|
</NButton>
|
||||||
<NButton
|
<NButton
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -116,8 +275,10 @@ const streamerOptions = computed(() => {
|
|||||||
刷新记录
|
刷新记录
|
||||||
</NButton>
|
</NButton>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
|
</NFlex>
|
||||||
|
|
||||||
<NEmpty
|
<NEmpty
|
||||||
v-if="filteredHistory.length == 0"
|
v-if="filteredHistory.length === 0"
|
||||||
description="暂无符合条件的积分记录"
|
description="暂无符合条件的积分记录"
|
||||||
/>
|
/>
|
||||||
<PointHistoryCard
|
<PointHistoryCard
|
||||||
@@ -126,3 +287,34 @@ const streamerOptions = computed(() => {
|
|||||||
/>
|
/>
|
||||||
</NSpin>
|
</NSpin>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color-1);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.primary {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.success {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.error {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color-3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -327,7 +327,12 @@ onMounted(async () => {
|
|||||||
justify="center"
|
justify="center"
|
||||||
>
|
>
|
||||||
<div style="max-width: 95vw; width: 1200px">
|
<div style="max-width: 95vw; width: 1200px">
|
||||||
<NCard title="我的信息">
|
<NCard
|
||||||
|
title="我的信息"
|
||||||
|
:bordered="false"
|
||||||
|
size="small"
|
||||||
|
class="info-card"
|
||||||
|
>
|
||||||
<NDescriptions
|
<NDescriptions
|
||||||
label-placement="left"
|
label-placement="left"
|
||||||
bordered
|
bordered
|
||||||
@@ -350,24 +355,27 @@ onMounted(async () => {
|
|||||||
<NTag
|
<NTag
|
||||||
v-if="biliAuth.id > 0"
|
v-if="biliAuth.id > 0"
|
||||||
type="success"
|
type="success"
|
||||||
|
size="small"
|
||||||
>
|
>
|
||||||
已认证
|
已认证
|
||||||
</NTag>
|
</NTag>
|
||||||
<NTag
|
<NTag
|
||||||
v-else
|
v-else
|
||||||
type="error"
|
type="error"
|
||||||
|
size="small"
|
||||||
>
|
>
|
||||||
未认证
|
未认证
|
||||||
</NTag>
|
</NTag>
|
||||||
</NDescriptionsItem>
|
</NDescriptionsItem>
|
||||||
</NDescriptions>
|
</NDescriptions>
|
||||||
</NCard>
|
</NCard>
|
||||||
<NDivider />
|
<NDivider style="margin: 16px 0" />
|
||||||
<NTabs
|
<NTabs
|
||||||
v-if="hash"
|
v-if="hash"
|
||||||
v-model:value="hash"
|
v-model:value="hash"
|
||||||
default-value="points"
|
default-value="points"
|
||||||
animated
|
animated
|
||||||
|
type="line"
|
||||||
@update:value="onTabChange"
|
@update:value="onTabChange"
|
||||||
>
|
>
|
||||||
<NTabPane
|
<NTabPane
|
||||||
@@ -440,3 +448,54 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</NLayout>
|
</NLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.info-card {
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tabs-nav) {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tabs-tab) {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tabs-tab:hover) {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tab-pane) {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端优化 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:deep(.n-layout-header) {
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-layout-content) {
|
||||||
|
padding: 16px 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-descriptions) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tabs-nav) {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画优化 */
|
||||||
|
:deep(.n-spin-container) {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -274,33 +274,48 @@ defineExpose({
|
|||||||
<NFlex
|
<NFlex
|
||||||
justify="center"
|
justify="center"
|
||||||
align="center"
|
align="center"
|
||||||
|
vertical
|
||||||
|
:gap="16"
|
||||||
>
|
>
|
||||||
<NCard
|
<NCard
|
||||||
title="更多"
|
title="更多"
|
||||||
embedded
|
embedded
|
||||||
|
style="width: 100%; max-width: 800px"
|
||||||
>
|
>
|
||||||
<NCollapse>
|
<NCollapse>
|
||||||
<NCollapseItem
|
<NCollapseItem
|
||||||
title="收货地址"
|
title="收货地址"
|
||||||
name="1"
|
name="1"
|
||||||
>
|
>
|
||||||
<NFlex vertical>
|
<NFlex
|
||||||
|
vertical
|
||||||
|
:gap="12"
|
||||||
|
>
|
||||||
<NButton
|
<NButton
|
||||||
type="primary"
|
type="primary"
|
||||||
|
block
|
||||||
@click="onOpenAddressModal"
|
@click="onOpenAddressModal"
|
||||||
>
|
>
|
||||||
添加地址
|
添加地址
|
||||||
</NButton>
|
</NButton>
|
||||||
|
<NEmpty
|
||||||
|
v-if="!biliAuth.address || biliAuth.address.length === 0"
|
||||||
|
description="暂无收货地址"
|
||||||
|
style="margin: 20px 0"
|
||||||
|
/>
|
||||||
<NList
|
<NList
|
||||||
|
v-else
|
||||||
size="small"
|
size="small"
|
||||||
bordered
|
bordered
|
||||||
>
|
>
|
||||||
<NListItem
|
<NListItem
|
||||||
v-for="address in biliAuth.address"
|
v-for="address in biliAuth.address"
|
||||||
:key="address.id"
|
:key="address.id"
|
||||||
|
class="address-item"
|
||||||
>
|
>
|
||||||
<AddressDisplay :address="address">
|
<AddressDisplay :address="address">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
|
<NFlex :gap="8">
|
||||||
<NButton
|
<NButton
|
||||||
size="small"
|
size="small"
|
||||||
type="info"
|
type="info"
|
||||||
@@ -323,6 +338,7 @@ defineExpose({
|
|||||||
</template>
|
</template>
|
||||||
确定要删除这个收货信息吗?
|
确定要删除这个收货信息吗?
|
||||||
</NPopconfirm>
|
</NPopconfirm>
|
||||||
|
</NFlex>
|
||||||
</template>
|
</template>
|
||||||
</AddressDisplay>
|
</AddressDisplay>
|
||||||
</NListItem>
|
</NListItem>
|
||||||
@@ -333,49 +349,81 @@ defineExpose({
|
|||||||
title="登录链接"
|
title="登录链接"
|
||||||
name="2"
|
name="2"
|
||||||
>
|
>
|
||||||
|
<NFlex
|
||||||
|
vertical
|
||||||
|
:gap="8"
|
||||||
|
>
|
||||||
|
<NText depth="3">
|
||||||
|
使用此链接可以直接登录到您的账号
|
||||||
|
</NText>
|
||||||
<NInput
|
<NInput
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:value="`${CURRENT_HOST}bili-user?auth=${useAuth.biliToken}`"
|
:value="`${CURRENT_HOST}bili-user?auth=${useAuth.biliToken}`"
|
||||||
readonly
|
readonly
|
||||||
|
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||||
/>
|
/>
|
||||||
|
</NFlex>
|
||||||
</NCollapseItem>
|
</NCollapseItem>
|
||||||
</NCollapse>
|
</NCollapse>
|
||||||
</NCard>
|
</NCard>
|
||||||
<NCard
|
<NCard
|
||||||
title="账号操作"
|
title="账号操作"
|
||||||
embedded
|
embedded
|
||||||
|
style="width: 100%; max-width: 800px"
|
||||||
|
>
|
||||||
|
<NFlex
|
||||||
|
vertical
|
||||||
|
:gap="12"
|
||||||
>
|
>
|
||||||
<NFlex>
|
|
||||||
<NPopconfirm @positive-click="logout">
|
<NPopconfirm @positive-click="logout">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton
|
<NButton
|
||||||
type="warning"
|
type="warning"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
登出
|
登出当前账号
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
确定要登出吗?
|
确定要登出吗?
|
||||||
</NPopconfirm>
|
</NPopconfirm>
|
||||||
</NFlex>
|
<NDivider style="margin: 8px 0">
|
||||||
<NDivider> 切换账号 </NDivider>
|
切换账号
|
||||||
|
</NDivider>
|
||||||
|
<NEmpty
|
||||||
|
v-if="useAuth.biliTokens.length === 0"
|
||||||
|
description="暂无其他账号"
|
||||||
|
/>
|
||||||
<NList
|
<NList
|
||||||
|
v-else
|
||||||
clickable
|
clickable
|
||||||
bordered
|
bordered
|
||||||
>
|
>
|
||||||
<NListItem
|
<NListItem
|
||||||
v-for="item in useAuth.biliTokens"
|
v-for="item in useAuth.biliTokens"
|
||||||
:key="item.token"
|
:key="item.token"
|
||||||
|
class="account-item"
|
||||||
|
:class="{ 'current-account': useAuth.biliToken === item.token }"
|
||||||
@click="switchAuth(item.token)"
|
@click="switchAuth(item.token)"
|
||||||
>
|
>
|
||||||
<NFlex align="center">
|
<NFlex
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<NFlex
|
||||||
|
align="center"
|
||||||
|
:gap="8"
|
||||||
|
>
|
||||||
<NTag
|
<NTag
|
||||||
v-if="useAuth.biliToken == item.token"
|
v-if="useAuth.biliToken === item.token"
|
||||||
type="info"
|
type="success"
|
||||||
|
size="small"
|
||||||
>
|
>
|
||||||
当前账号
|
当前账号
|
||||||
</NTag>
|
</NTag>
|
||||||
|
<NText strong>
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
|
</NText>
|
||||||
<NDivider
|
<NDivider
|
||||||
vertical
|
vertical
|
||||||
style="margin: 0"
|
style="margin: 0"
|
||||||
@@ -384,8 +432,10 @@ defineExpose({
|
|||||||
{{ item.uId }}
|
{{ item.uId }}
|
||||||
</NText>
|
</NText>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
|
</NFlex>
|
||||||
</NListItem>
|
</NListItem>
|
||||||
</NList>
|
</NList>
|
||||||
|
</NFlex>
|
||||||
</NCard>
|
</NCard>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</NSpin>
|
</NSpin>
|
||||||
@@ -403,18 +453,23 @@ defineExpose({
|
|||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="currentAddress"
|
:model="currentAddress"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-placement="top"
|
||||||
>
|
>
|
||||||
<NFormItem
|
<NFormItem
|
||||||
label="地址"
|
label="地区选择"
|
||||||
path="area"
|
path="area"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<NFlex style="width: 100%">
|
<NFlex
|
||||||
|
style="width: 100%"
|
||||||
|
:gap="8"
|
||||||
|
wrap
|
||||||
|
>
|
||||||
<NSelect
|
<NSelect
|
||||||
v-model:value="currentAddress.province"
|
v-model:value="currentAddress.province"
|
||||||
:options="provinceOptions"
|
:options="provinceOptions"
|
||||||
placeholder="请选择省"
|
placeholder="省"
|
||||||
style="width: 100px"
|
style="flex: 1; min-width: 100px"
|
||||||
filterable
|
filterable
|
||||||
@update:value="onAreaSelectChange(0)"
|
@update:value="onAreaSelectChange(0)"
|
||||||
/>
|
/>
|
||||||
@@ -423,8 +478,8 @@ defineExpose({
|
|||||||
v-model:value="currentAddress.city"
|
v-model:value="currentAddress.city"
|
||||||
:options="cityOptions(currentAddress.province)"
|
:options="cityOptions(currentAddress.province)"
|
||||||
:disabled="!currentAddress?.province"
|
:disabled="!currentAddress?.province"
|
||||||
placeholder="请选择市"
|
placeholder="市"
|
||||||
style="width: 100px"
|
style="flex: 1; min-width: 100px"
|
||||||
filterable
|
filterable
|
||||||
@update:value="onAreaSelectChange(1)"
|
@update:value="onAreaSelectChange(1)"
|
||||||
/>
|
/>
|
||||||
@@ -433,8 +488,8 @@ defineExpose({
|
|||||||
v-model:value="currentAddress.district"
|
v-model:value="currentAddress.district"
|
||||||
:options="currentAddress.city ? districtOptions(currentAddress.province, currentAddress.city) : []"
|
:options="currentAddress.city ? districtOptions(currentAddress.province, currentAddress.city) : []"
|
||||||
:disabled="!currentAddress?.city"
|
:disabled="!currentAddress?.city"
|
||||||
placeholder="请选择区"
|
placeholder="区"
|
||||||
style="width: 100px"
|
style="flex: 1; min-width: 100px"
|
||||||
filterable
|
filterable
|
||||||
@update:value="onAreaSelectChange(2)"
|
@update:value="onAreaSelectChange(2)"
|
||||||
/>
|
/>
|
||||||
@@ -443,8 +498,8 @@ defineExpose({
|
|||||||
v-model:value="currentAddress.street"
|
v-model:value="currentAddress.street"
|
||||||
:options="currentAddress.city && currentAddress.district ? streetOptions(currentAddress.province, currentAddress.city, currentAddress.district) : []"
|
:options="currentAddress.city && currentAddress.district ? streetOptions(currentAddress.province, currentAddress.city, currentAddress.district) : []"
|
||||||
:disabled="!currentAddress?.district"
|
:disabled="!currentAddress?.district"
|
||||||
placeholder="请选择街道"
|
placeholder="街道"
|
||||||
style="width: 150px"
|
style="flex: 1; min-width: 120px"
|
||||||
filterable
|
filterable
|
||||||
/>
|
/>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
@@ -456,39 +511,43 @@ defineExpose({
|
|||||||
>
|
>
|
||||||
<NInput
|
<NInput
|
||||||
v-model:value="currentAddress.address"
|
v-model:value="currentAddress.address"
|
||||||
placeholder="详细地址"
|
placeholder="请输入详细地址(楼栋号、单元号、门牌号等)"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||||
/>
|
/>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
|
<NFlex :gap="12">
|
||||||
<NFormItem
|
<NFormItem
|
||||||
label="联系电话"
|
label="联系电话"
|
||||||
path="phone"
|
path="phone"
|
||||||
required
|
required
|
||||||
|
style="flex: 1"
|
||||||
>
|
>
|
||||||
<NInputNumber
|
<NInputNumber
|
||||||
v-model:value="currentAddress.phone"
|
v-model:value="currentAddress.phone"
|
||||||
placeholder="联系电话"
|
placeholder="请输入联系电话"
|
||||||
:show-button="false"
|
:show-button="false"
|
||||||
style="width: 200px"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem
|
<NFormItem
|
||||||
label="联系人"
|
label="联系人"
|
||||||
path="name"
|
path="name"
|
||||||
required
|
required
|
||||||
|
style="flex: 1"
|
||||||
>
|
>
|
||||||
<NInput
|
<NInput
|
||||||
v-model:value="currentAddress.name"
|
v-model:value="currentAddress.name"
|
||||||
placeholder="联系人"
|
placeholder="请输入联系人姓名"
|
||||||
style="max-width: 150px"
|
|
||||||
/>
|
/>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
|
</NFlex>
|
||||||
<NFormItem
|
<NFormItem
|
||||||
label="用户协议"
|
label="用户协议"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<NCheckbox v-model:checked="userAgree">
|
<NCheckbox v-model:checked="userAgree">
|
||||||
阅读并同意本站
|
我已阅读并同意本站
|
||||||
<NButton
|
<NButton
|
||||||
text
|
text
|
||||||
type="info"
|
type="info"
|
||||||
@@ -498,13 +557,23 @@ defineExpose({
|
|||||||
</NButton>
|
</NButton>
|
||||||
</NCheckbox>
|
</NCheckbox>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
|
<NFlex
|
||||||
|
justify="end"
|
||||||
|
:gap="12"
|
||||||
|
>
|
||||||
<NButton
|
<NButton
|
||||||
type="info"
|
@click="showAddressModal = false"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</NButton>
|
||||||
|
<NButton
|
||||||
|
type="primary"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
@click="updateAddress"
|
@click="updateAddress"
|
||||||
>
|
>
|
||||||
保存
|
保存
|
||||||
</NButton>
|
</NButton>
|
||||||
|
</NFlex>
|
||||||
</NForm>
|
</NForm>
|
||||||
</NSpin>
|
</NSpin>
|
||||||
</NModal>
|
</NModal>
|
||||||
@@ -519,3 +588,37 @@ defineExpose({
|
|||||||
</NScrollbar>
|
</NScrollbar>
|
||||||
</NModal>
|
</NModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.address-item {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-item:hover {
|
||||||
|
background-color: var(--hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-item {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-item:hover {
|
||||||
|
background-color: var(--hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-account {
|
||||||
|
background-color: var(--primary-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端优化 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:deep(.n-card) {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-form-item-label) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Markdown from 'unplugin-vue-markdown/vite'
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import svgLoader from 'vite-svg-loader'
|
import svgLoader from 'vite-svg-loader'
|
||||||
import { VineVitePlugin } from 'vue-vine/vite'
|
import { VineVitePlugin } from 'vue-vine/vite'
|
||||||
|
// import MonacoEditorNlsPlugin, { esbuildPluginMonacoEditorNls, Languages } from 'vite-plugin-monaco-editor-nls'
|
||||||
|
|
||||||
// 自定义SVGO插件,删除所有名称以sodipodi:和inkscape:开头的元素
|
// 自定义SVGO插件,删除所有名称以sodipodi:和inkscape:开头的元素
|
||||||
const removeSodipodiInkscape = {
|
const removeSodipodiInkscape = {
|
||||||
@@ -87,6 +88,8 @@ export default defineConfig({
|
|||||||
include: [/\.vue$/, /\.vue\?vue/, /\.md$/, /\.vine$/],
|
include: [/\.vue$/, /\.vue\?vue/, /\.md$/, /\.vine$/],
|
||||||
}),
|
}),
|
||||||
VineVitePlugin(),
|
VineVitePlugin(),
|
||||||
|
// Monaco 中文本地化
|
||||||
|
// MonacoEditorNlsPlugin({ locale: Languages.zh_hans }),
|
||||||
],
|
],
|
||||||
server: { port: 51000 },
|
server: { port: 51000 },
|
||||||
resolve: { alias: { '@': path.resolve(__dirname, 'src') } },
|
resolve: { alias: { '@': path.resolve(__dirname, 'src') } },
|
||||||
@@ -97,9 +100,15 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: ['@vicons/fluent', '@vicons/ionicons5', 'vue', 'vue-router'],
|
include: ['@vicons/fluent', '@vicons/ionicons5', 'vue', 'vue-router'],
|
||||||
|
esbuildOptions: {
|
||||||
|
// plugins: [
|
||||||
|
// esbuildPluginMonacoEditorNls({ locale: Languages.zh_hans }),
|
||||||
|
// ],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
sourcemap: true,
|
// 生产环境建议关闭以减少产物体积与网络请求
|
||||||
|
sourcemap: false,
|
||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
minify: 'oxc',
|
minify: 'oxc',
|
||||||
chunkSizeWarningLimit: 1000,
|
chunkSizeWarningLimit: 1000,
|
||||||
@@ -117,6 +126,37 @@ export default defineConfig({
|
|||||||
test: /[\\/]node_modules[\\/](naive-ui|@vueuse[\\/]core)[\\/]/,
|
test: /[\\/]node_modules[\\/](naive-ui|@vueuse[\\/]core)[\\/]/,
|
||||||
priority: -10,
|
priority: -10,
|
||||||
},
|
},
|
||||||
|
// 精细化切分大体积依赖,提升缓存与首屏体积可控性
|
||||||
|
{
|
||||||
|
name: 'echarts-vendor',
|
||||||
|
test: /[\\/]node_modules[\\/](echarts|zrender|vue-echarts)[\\/]/,
|
||||||
|
priority: -20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'wangeditor-vendor',
|
||||||
|
test: /[\\/]node_modules[\\/]@wangeditor[\\/]/,
|
||||||
|
priority: -20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hyperdx-vendor',
|
||||||
|
test: /[\\/]node_modules[\\/]@hyperdx[\\/]/,
|
||||||
|
priority: -20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'xlsx-vendor',
|
||||||
|
test: /[\\/]node_modules[\\/]xlsx[\\/]/,
|
||||||
|
priority: -20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'jszip-vendor',
|
||||||
|
test: /[\\/]node_modules[\\/]jszip[\\/]/,
|
||||||
|
priority: -20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'html2canvas-vendor',
|
||||||
|
test: /[\\/]node_modules[\\/]html2canvas[\\/]/,
|
||||||
|
priority: -20,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user