feat: 更新依赖和组件,优化功能实现

- 更新package.json中的TypeScript版本
- 删除不再使用的message_render_content.txt文件
- 在VEditor.vue中移除未使用的常量导入
- 在OBSComponentStoreView.vue中修复用户信息的属性引用
- 在SvgInspector.vue中删除不再使用的组件
- 在PointManage.vue中优化商品模型和表单验证逻辑
- 在WebFetcherOBS.vue中修复webfetcher类型的引用
- 在DefaultIndexTemplate.vue中更新封面配置项类型
This commit is contained in:
Megghy
2025-05-17 15:58:53 +08:00
parent 1ae528b9a9
commit 5db66e3861
12 changed files with 464 additions and 881 deletions

BIN
bun.lockb

Binary file not shown.

Binary file not shown.

View File

@@ -85,7 +85,7 @@
"eslint": "^9.26.0",
"eslint-plugin-vue": "^10.1.0",
"stylus": "^0.64.0",
"typescript": "^5.8.3",
"typescript": "^5.9.0-dev.20250512",
"vue-vine": "^0.4.4"
}
}

View File

@@ -3,12 +3,13 @@ import { isDarkMode } from '@/Utils'
import { APIRoot } from '@/api/api-models'
import { GetHeaders } from '@/api/query'
import '@/assets/editorDarkMode.css'
import { BASE_URL, VTSURU_API_URL } from '@/data/constants'
import { VTSURU_API_URL } from '@/data/constants'
import { DomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
// @ts-ignore
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import { NotificationReactive, useMessage } from 'naive-ui'
import { onBeforeUnmount, ref, shallowRef, onMounted } from 'vue'
import { onBeforeUnmount, ref, shallowRef } from 'vue'
type InsertFnType = (url: string, alt: string, href: string) => void

View File

@@ -471,5 +471,6 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
// 实例 (谨慎暴露,主要用于调试或特定场景)
signalRClient: computed(() => signalRClient.value), // 返回计算属性以防直接修改
client
};
});

View File

@@ -615,6 +615,7 @@ function parseExcelFile() {
* 解析多值字段(如作者、标签等)
*/
function parseMultipleValues(value: string): string[] {
if (!value) return []
return value
?.replace('', '/')
.replace('', ',')

View File

@@ -147,7 +147,7 @@ const accountInfo = useAccount();
const biliAuth = useBiliAuth(); // 若需要B站授权信息
const message = useMessage();
const userInfo = ref<UserInfo | undefined>(accountInfo.value.id ? { id: accountInfo.value.id, name: accountInfo.value.username } as UserInfo : undefined); // 模拟
const userInfo = ref<UserInfo | undefined>(accountInfo.value.id ? { id: accountInfo.value.id, name: accountInfo.value.name } as UserInfo : undefined); // 模拟
const availableComponents = ref<OBSComponentDefinition[]>([]);
const currentSelectedComponentId = ref<string | null>(null);
@@ -234,7 +234,7 @@ async function loadComponentConfig(settingName: string) {
isLoading.value = true;
try {
const configData = await DownloadConfig(settingName, userInfo.value.id);
const configData = await DownloadConfig<any>(settingName, userInfo.value.id);
const defaultConfig = dynamicComponentRef.value?.DefaultConfig || {};
if (configData.msg || Object.keys(configData.data || {}).length === 0) {
@@ -242,7 +242,7 @@ async function loadComponentConfig(settingName: string) {
message.info('未找到在线配置,已加载默认配置。');
} else {
// 合并远程配置和默认配置,确保所有键都存在
componentConfig.value = { ...defaultConfig, ...configData.data };
componentConfig.value = configData.data;
}
} catch (error) {
console.error('加载组件配置失败:', error);
@@ -261,11 +261,9 @@ async function saveComponentConfig() {
}
isLoading.value = true;
try {
await UploadConfig({
name: currentSelectedComponent.value.settingName,
config: JSON.stringify(componentConfigForEditing.value), // 保存编辑后的配置
isPublic: false, // 或根据需要设置为 true
});
await UploadConfig(currentSelectedComponent.value.settingName,
JSON.stringify(componentConfigForEditing.value), // 保存编辑后的配置
false) // 或根据需要设置为 true);
message.success('配置保存成功!');
componentConfig.value = JSON.parse(JSON.stringify(componentConfigForEditing.value)); // 更新运行时配置
showSettingModal.value = false;

View File

@@ -4,7 +4,7 @@
class="example-obs-component"
>
<NAlert
:type="localConfig.alertType || 'info'"
:type="localConfig.alertType as any || 'info'"
:title="localConfig.alertTitle || '组件信息'"
>
<p>{{ localConfig.contentText || '这是示例 OBS 组件的内容。' }}</p>
@@ -31,7 +31,7 @@
<script lang="ts">
// Moved Config and DefaultConfig here to avoid linter errors with <script setup>
import { ConfigItemType, defineTemplateConfig, ExtractConfigData } from '@/data/VTsuruConfigTypes';
import { defineTemplateConfig, ExtractConfigData } from '@/data/VTsuruConfigTypes';
export const Config = defineTemplateConfig([
{
@@ -74,15 +74,6 @@ export const Config = defineTemplateConfig([
type: 'boolean',
default: false,
},
{
name: '刷新间隔 (秒)',
key: 'refreshInterval',
type: ConfigItemType.Number,
default: 60,
min: 5,
description: '组件自动刷新的时间间隔(如果组件支持自动刷新)。',
if: (config: ExampleConfigType) => config.enableAdvanced === true, // 条件显示
}
]);
export type ExampleConfigType = ExtractConfigData<typeof Config>;
@@ -93,7 +84,6 @@ export const DefaultConfig: ExampleConfigType = {
alertTitle: '默认提示',
contentText: '来自 DefaultConfig 的内容。点歌点歌点歌。关注vtsuru喵',
enableAdvanced: false,
refreshInterval: 30,
};
</script>

View File

@@ -1,404 +0,0 @@
<template>
<n-card class="svg-inspector">
<n-h3>SVG结构检查工具</n-h3>
<n-space
align="center"
wrap
size="small"
>
<n-select
v-model:value="selectedType"
:options="controllerOptions"
style="min-width: 150px"
/>
<n-select
v-if="availableBodies.length > 0"
v-model:value="selectedBodyId"
:options="bodyOptions"
style="min-width: 150px"
/>
<n-button
:disabled="isInspecting"
type="primary"
@click="inspectSvg"
>
分析SVG结构
</n-button>
</n-space>
<n-spin
v-if="isInspecting"
size="medium"
style="margin: 20px 0"
>
<n-text>正在分析SVG结构...</n-text>
</n-spin>
<div
v-if="svgInfo.length > 0"
class="results"
>
<n-h4>检测到的元素 ({{ svgInfo.length }}):</n-h4>
<n-space
vertical
size="small"
>
<n-space
align="center"
size="medium"
>
<n-input
v-model:value="searchFilter"
placeholder="搜索元素..."
style="min-width: 250px"
/>
<n-checkbox v-model:checked="showLabelsOnly">
只显示带标签的元素
</n-checkbox>
</n-space>
<n-scrollbar class="results-scrollbar">
<n-space
vertical
size="small"
>
<n-card
v-for="(item, index) in filteredSvgInfo"
:key="index"
size="small"
:bordered="false"
style="padding: 12px"
:class="{ 'has-label': item.label, 'element-card': true }"
@click="selectElement(item)"
>
<n-space
vertical
size="small"
>
<n-text><strong>ID:</strong> {{ item.id || '(无ID)' }}</n-text>
<n-text v-if="item.label">
<strong>标签:</strong> {{ item.label }}
</n-text>
<n-text><strong>类型:</strong> {{ item.tagName }}</n-text>
<n-space
vertical
size="small"
class="hide-rule"
>
<n-space size="small">
<n-button
size="tiny"
@click.stop="selectElement(item)"
>
选择此元素
</n-button>
<n-button
size="tiny"
@click.stop="highlightElement(item)"
>
在SVG中高亮
</n-button>
</n-space>
<n-code
v-if="item.id"
:code="getCssRuleForId(item.id)"
language="css"
show-line-numbers
size="small"
/>
<n-code
v-if="item.label"
:code="getCssRuleForLabel(item.label)"
language="css"
show-line-numbers
size="small"
/>
</n-space>
</n-space>
</n-card>
</n-space>
</n-scrollbar>
</n-space>
</div>
</n-card>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';
import { gamepadConfigs, controllerBodies } from '@/data/gamepadConfigs';
import type { GamepadType } from '@/types/gamepad';
import {
NCard,
NSpace,
NSelect,
NButton,
NSpin,
NInput,
NCheckbox,
NText,
NH3,
NH4,
NCode,
NScrollbar
} from 'naive-ui';
interface Props {
controllerType?: GamepadType;
}
const props = withDefaults(defineProps<Props>(), {
controllerType: 'xbox',
});
interface SvgElementInfo {
id: string | null;
label: string | null;
tagName: string;
fill?: string;
stroke?: string;
transform?: string;
element?: Element; // 添加对实际DOM元素的引用
visible: boolean; // 添加visible属性
}
const selectedType = ref<GamepadType>(props.controllerType);
const selectedBodyId = ref('');
const isInspecting = ref(false);
const svgInfo = ref<SvgElementInfo[]>([]);
const searchFilter = ref('');
const showLabelsOnly = ref(false);
const highlightedElement = ref<Element | null>(null);
// 控制器类型选项
const controllerOptions = [
{ label: 'Xbox控制器', value: 'xbox' },
{ label: 'PlayStation控制器', value: 'ps' },
{ label: 'Nintendo控制器', value: 'nintendo' }
];
// 定义emit事件
const emit = defineEmits<{
(e: 'element-selected', element: SvgElementInfo): void
}>();
// 获取当前控制器类型的所有可用主体
const availableBodies = computed(() => {
return controllerBodies[selectedType.value] || [];
});
// 转换为NSelect需要的选项格式
const bodyOptions = computed(() => {
const options = availableBodies.value.map(body => ({
label: body.name,
value: body.id
}));
return [
{ label: '(默认主体)', value: '' },
...options
];
});
// 为n-code生成CSS代码字符串
function getCssRuleForId(id: string) {
return `.custom-visibility :deep([id="${id}"]) { visibility: visible; }`;
}
function getCssRuleForLabel(label: string) {
return `.custom-visibility :deep([inkscape\\:label="${label}"]) { visibility: visible; }`;
}
// 根据选择的主体获取对应的SVG组件
const currentBodySvg = computed(() => {
if (selectedBodyId.value) {
const selectedBody = availableBodies.value.find(b => b.id === selectedBodyId.value);
if (selectedBody) return selectedBody.body;
}
// 如果未选择特定主体或找不到所选主体,则使用默认主体
return gamepadConfigs[selectedType.value]?.bodySvg;
});
// 监听props变化更新选择的控制器类型
watch(() => props.controllerType, (newType) => {
selectedType.value = newType;
// 切换控制器类型时重置所选主体
selectedBodyId.value = '';
}, { immediate: true });
// 自动分析当前控制器类型
onMounted(() => {
// 延迟执行以确保组件完全挂载
setTimeout(() => {
inspectSvg();
}, 500);
});
const filteredSvgInfo = computed(() => {
return svgInfo.value.filter(item => {
// 筛选显示带标签的元素
if (showLabelsOnly.value && !item.label) {
return false;
}
// 搜索过滤
const filter = searchFilter.value.toLowerCase();
if (!filter) return true;
return (
(item.id && item.id.toLowerCase().includes(filter)) ||
(item.label && item.label.toLowerCase().includes(filter)) ||
item.tagName.toLowerCase().includes(filter)
);
});
});
// 选择元素并发送到父组件
function selectElement(item: SvgElementInfo) {
// 移除之前高亮的元素
clearHighlight();
// 发出选中事件
emit('element-selected', item);
}
// 在SVG中高亮显示元素
function highlightElement(item: SvgElementInfo) {
// 清除之前的高亮
clearHighlight();
if (item.element) {
// 保存原始样式
const originalStyle = item.element.getAttribute('style') || '';
item.element.setAttribute('data-original-style', originalStyle);
// 应用高亮样式
item.element.setAttribute('style', `${originalStyle}; outline: 2px solid red; outline-offset: 2px;`);
highlightedElement.value = item.element;
// 5秒后自动清除高亮
setTimeout(() => {
clearHighlight();
}, 5000);
}
}
// 清除高亮
function clearHighlight() {
if (highlightedElement.value) {
const originalStyle = highlightedElement.value.getAttribute('data-original-style') || '';
highlightedElement.value.setAttribute('style', originalStyle);
highlightedElement.value.removeAttribute('data-original-style');
highlightedElement.value = null;
}
}
// 分析SVG结构
async function inspectSvg() {
try {
isInspecting.value = true;
svgInfo.value = [];
const svgComponent = currentBodySvg.value;
if (!svgComponent) {
throw new Error(`找不到${selectedType.value}控制器的SVG配置`);
}
// 创建一个临时元素来加载SVG
const tempContainer = document.createElement('div');
tempContainer.style.position = 'absolute';
tempContainer.style.visibility = 'hidden';
document.body.appendChild(tempContainer);
// 使用Vue组件加载SVG (需要异步处理)
const { createApp, h } = await import('vue');
const app = createApp({
render: () => h(svgComponent)
});
app.mount(tempContainer);
// 等待SVG呈现 (实际情况下可能需要更复杂的检测机制)
await new Promise(resolve => setTimeout(resolve, 100));
// 现在分析临时容器中的SVG结构
const svgElement = tempContainer.querySelector('svg');
if (!svgElement) {
throw new Error('无法在组件中找到SVG元素');
}
// 递归收集SVG元素的信息
collectSvgElements(svgElement);
// 清理
app.unmount();
document.body.removeChild(tempContainer);
isInspecting.value = false;
} catch (error) {
console.error('SVG分析失败:', error);
isInspecting.value = false;
}
}
// 递归收集SVG元素信息
function collectSvgElements(element: Element) {
// 检查是否有ID或inkscape:label
const id = element.id;
const label = element.getAttribute('inkscape:label');
const computedStyle = window.getComputedStyle(element);
// 如果有ID或标签添加到列表中
if (id || label) {
svgInfo.value.push({
id,
label,
tagName: element.tagName.toLowerCase(),
fill: computedStyle.fill !== 'none' ? computedStyle.fill : undefined,
stroke: computedStyle.stroke !== 'none' ? computedStyle.stroke : undefined,
transform: element.getAttribute('transform') || undefined,
element, // 保存对DOM元素的引用
visible: true // 默认可见
});
}
// 递归处理子元素
Array.from(element.children).forEach(child => {
collectSvgElements(child);
});
}
</script>
<style scoped>
.svg-inspector {
margin-bottom: 20px;
max-height: 100vh;
overflow: auto;
}
.has-label {
border-left: 3px solid #18a058 !important;
}
.results {
margin-top: 15px;
}
.element-card {
cursor: pointer;
transition: background-color 0.2s;
}
.element-card:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.hide-rule {
margin-top: 10px;
}
.results-scrollbar {
max-height: calc(80vh - 200px);
overflow: auto;
}
</style>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { copyToClipboard } from '@/Utils'
import { DisableFunction, EnableFunction, useAccount } from '@/api/account'
import { copyToClipboard } from '@/Utils';
import { DisableFunction, EnableFunction, useAccount } from '@/api/account';
import {
FunctionTypes,
GoodsStatus,
@@ -9,15 +9,15 @@ import {
ResponsePointGoodModel,
UploadPointGoodsModel,
UserFileLocation
} from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import EventFetcherStatusCard from '@/components/EventFetcherStatusCard.vue'
import PointGoodsItem from '@/components/manage/PointGoodsItem.vue'
import { CURRENT_HOST, POINT_API_URL } from '@/data/constants'
import { uploadFiles, UploadStage } from '@/data/fileUpload'
import { useBiliAuth } from '@/store/useBiliAuth'
import { Info24Filled } from '@vicons/fluent'
import { useRouteHash } from '@vueuse/router'
} from '@/api/api-models';
import { QueryGetAPI, QueryPostAPI } from '@/api/query';
import EventFetcherStatusCard from '@/components/EventFetcherStatusCard.vue';
import PointGoodsItem from '@/components/manage/PointGoodsItem.vue';
import { CURRENT_HOST, POINT_API_URL } from '@/data/constants';
import { uploadFiles, UploadStage } from '@/data/fileUpload';
import { useBiliAuth } from '@/store/useBiliAuth';
import { Info24Filled } from '@vicons/fluent';
import { useRouteHash } from '@vueuse/router';
import {
FormItemRule,
NAlert,
@@ -51,37 +51,37 @@ import {
UploadFileInfo,
useDialog,
useMessage
} from 'naive-ui'
import { computed, onMounted, ref, watch } from 'vue'
import PointOrderManage from './PointOrderManage.vue'
import PointSettings from './PointSettings.vue'
import PointUserManage from './PointUserManage.vue'
} from 'naive-ui';
import { computed, onMounted, ref, watch } from 'vue';
import PointOrderManage from './PointOrderManage.vue';
import PointSettings from './PointSettings.vue';
import PointUserManage from './PointUserManage.vue';
const message = useMessage()
const accountInfo = useAccount()
const dialog = useDialog()
const biliAuth = useBiliAuth()
const formRef = ref()
const isUpdating = ref(false)
const isAllowedPrivacyPolicy = ref(false)
const showAddGoodsModal = ref(false)
const uploadProgress = ref(0)
const isUploadingCover = ref(false)
const message = useMessage();
const accountInfo = useAccount();
const dialog = useDialog();
const biliAuth = useBiliAuth();
const formRef = ref();
const isUpdating = ref(false);
const isAllowedPrivacyPolicy = ref(false);
const showAddGoodsModal = ref(false);
const uploadProgress = ref(0);
const isUploadingCover = ref(false);
// 路由哈希处理
const realHash = useRouteHash('goods', { mode: 'replace' })
const realHash = useRouteHash('goods', { mode: 'replace' });
const hash = computed({
get() {
return realHash.value?.startsWith('#') ? realHash.value.slice(1) : realHash.value || 'goods'
return realHash.value?.startsWith('#') ? realHash.value.slice(1) : realHash.value || 'goods';
},
set(val) {
realHash.value = '#' + val
realHash.value = '#' + val;
},
})
});
// 商品数据及模型
const goods = ref<ResponsePointGoodModel[]>(await biliAuth.GetGoods(accountInfo.value?.id, message))
const defaultGoodsModel = (): { goods: UploadPointGoodsModel; fileList: UploadFileInfo[] } => ({
const goods = ref<ResponsePointGoodModel[]>(await biliAuth.GetGoods(accountInfo.value?.id, message));
const defaultGoodsModel = (): { goods: UploadPointGoodsModel; fileList: UploadFileInfo[]; } => ({
goods: {
type: GoodsTypes.Virtual,
status: GoodsStatus.Normal,
@@ -101,46 +101,46 @@ const defaultGoodsModel = (): { goods: UploadPointGoodsModel; fileList: UploadFi
cover: undefined,
} as UploadPointGoodsModel,
fileList: [],
})
const currentGoodsModel = ref<{ goods: UploadPointGoodsModel; fileList: UploadFileInfo[] }>(
});
const currentGoodsModel = ref<{ goods: UploadPointGoodsModel; fileList: UploadFileInfo[]; }>(
defaultGoodsModel()
)
);
// 监听 fileList 变化,确保 cover 和 fileList 同步
watch(() => currentGoodsModel.value.fileList, (newFileList, oldFileList) => {
if (oldFileList && oldFileList.length > 0 && newFileList.length === 0) {
if (currentGoodsModel.value.goods.id && currentGoodsModel.value.goods.cover) {
currentGoodsModel.value.goods.cover = undefined
currentGoodsModel.value.goods.cover = undefined;
}
}
}, { deep: true })
}, { deep: true });
// 计算属性
const allowedYearOptions = computed(() => {
return Array.from({ length: new Date().getFullYear() - 2024 + 1 }, (_, i) => 2024 + i).map((item) => ({
label: item.toString() + '年',
value: item,
}))
})
}));
});
const allowedMonthOptions = computed(() => {
return Array.from({ length: 12 }, (_, i) => i + 1).map((item) => ({
label: item.toString() + '月',
value: item,
}))
})
}));
});
const existTags = computed(() => {
if (goods.value.length === 0) return []
if (goods.value.length === 0) return [];
const tempSet = new Set<string>()
const tempSet = new Set<string>();
for (const good of goods.value) {
if (!good.tags || good.tags.length === 0) continue
good.tags.forEach(tag => tempSet.add(tag))
if (!good.tags || good.tags.length === 0) continue;
good.tags.forEach(tag => tempSet.add(tag));
}
return Array.from(tempSet).map(tag => ({ label: tag, value: tag }))
})
return Array.from(tempSet).map(tag => ({ label: tag, value: tag }));
});
// 下拉菜单选项
const dropDownActions = {
@@ -154,9 +154,9 @@ const dropDownActions = {
key: 'delete',
action: (item: ResponsePointGoodModel) => onDeleteClick(item),
},
} as { [key: string]: { label: string; key: string; action: (item: ResponsePointGoodModel) => void } }
} as { [key: string]: { label: string; key: string; action: (item: ResponsePointGoodModel) => void; }; };
const dropDownOptions = computed(() => Object.values(dropDownActions))
const dropDownOptions = computed(() => Object.values(dropDownActions));
// 表单验证规则
const rules = {
@@ -198,55 +198,55 @@ const rules = {
message: '请输入收集收货地址的链接',
validator: (rule: FormItemRule, value: string) => {
try {
new URL(value)
return true
new URL(value);
return true;
} catch (err) {
return false
return false;
}
},
},
}
};
// 方法
async function setFunctionEnable(enable: boolean) {
const success = enable ? await EnableFunction(FunctionTypes.Point) : await DisableFunction(FunctionTypes.Point)
const success = enable ? await EnableFunction(FunctionTypes.Point) : await DisableFunction(FunctionTypes.Point);
if (success) {
message.success('已' + (enable ? '启用' : '禁用') + '积分系统')
message.success('已' + (enable ? '启用' : '禁用') + '积分系统');
} else {
message.error('无法' + (enable ? '启用' : '禁用') + '积分系统')
message.error('无法' + (enable ? '启用' : '禁用') + '积分系统');
}
}
async function updateGoods(e: MouseEvent) {
if (isUpdating.value || !formRef.value) return
e.preventDefault()
isUpdating.value = true
isUploadingCover.value = false
uploadProgress.value = 0
if (isUpdating.value || !formRef.value) return;
e.preventDefault();
isUpdating.value = true;
isUploadingCover.value = false;
uploadProgress.value = 0;
try {
await formRef.value.validate()
await formRef.value.validate();
const newFilesToUpload = currentGoodsModel.value.fileList.filter(f => f.file && f.status !== 'finished')
const newFilesToUpload = currentGoodsModel.value.fileList.filter(f => f.file && f.status !== 'finished');
if (newFilesToUpload.length > 0 && newFilesToUpload[0].file) {
isUploadingCover.value = true
message.info('正在上传封面...')
isUploadingCover.value = true;
message.info('正在上传封面...');
const uploadResults = await uploadFiles(
[newFilesToUpload[0].file],
undefined,
UserFileLocation.Local,
(stage: string) => {
if (stage === UploadStage.Uploading) {
uploadProgress.value = 0
uploadProgress.value = 0;
}
}
)
isUploadingCover.value = false
);
isUploadingCover.value = false;
if (uploadResults && uploadResults.length > 0) {
currentGoodsModel.value.goods.cover = uploadResults[0]
message.success('封面上传成功')
const uploadedFileIndex = currentGoodsModel.value.fileList.findIndex(f => f.id === newFilesToUpload[0].id)
currentGoodsModel.value.goods.cover = uploadResults[0];
message.success('封面上传成功');
const uploadedFileIndex = currentGoodsModel.value.fileList.findIndex(f => f.id === newFilesToUpload[0].id);
if (uploadedFileIndex > -1) {
currentGoodsModel.value.fileList[uploadedFileIndex] = {
...currentGoodsModel.value.fileList[uploadedFileIndex],
@@ -257,47 +257,47 @@ async function updateGoods(e: MouseEvent) {
};
}
} else {
throw new Error('封面上传失败')
throw new Error('封面上传失败');
}
} else if (currentGoodsModel.value.fileList.length === 0 && currentGoodsModel.value.goods.id) {
currentGoodsModel.value.goods.cover = undefined
currentGoodsModel.value.goods.cover = undefined;
}
const { code, data, message: errMsg } = await QueryPostAPI<ResponsePointGoodModel>(
POINT_API_URL + 'update-goods',
currentGoodsModel.value.goods
)
);
if (code === 200) {
message.success('商品信息保存成功')
showAddGoodsModal.value = false
currentGoodsModel.value = defaultGoodsModel()
message.success('商品信息保存成功');
showAddGoodsModal.value = false;
currentGoodsModel.value = defaultGoodsModel();
const index = goods.value.findIndex(g => g.id === data.id)
const index = goods.value.findIndex(g => g.id === data.id);
if (index >= 0) {
goods.value[index] = data
goods.value[index] = data;
} else {
goods.value.push(data)
goods.value.push(data);
}
} else {
message.error('商品信息保存失败: ' + errMsg)
message.error('商品信息保存失败: ' + errMsg);
}
} catch (err: any) {
console.error(currentGoodsModel.value, err)
const errorMsg = err instanceof Error ? err.message : typeof err === 'string' ? err : '表单验证失败或上传出错'
message.error(`失败: ${errorMsg}`)
console.error(currentGoodsModel.value, err);
const errorMsg = err instanceof Error ? err.message : typeof err === 'string' ? err : '表单验证失败或上传出错';
message.error(`失败: ${errorMsg}`);
} finally {
isUpdating.value = false
isUploadingCover.value = false
isUpdating.value = false;
isUploadingCover.value = false;
}
}
function OnFileListChange(files: UploadFileInfo[]) {
if (files.length === 1 && (files[0].file?.size ?? 0) > 10 * 1024 * 1024) {
message.error('文件大小不能超过10MB')
currentGoodsModel.value.fileList = []
message.error('文件大小不能超过10MB');
currentGoodsModel.value.fileList = [];
} else {
currentGoodsModel.value.fileList = files
currentGoodsModel.value.fileList = files;
}
}
@@ -317,9 +317,9 @@ function onUpdateClick(item: ResponsePointGoodModel) {
},
]
: [],
}
isAllowedPrivacyPolicy.value = true
showAddGoodsModal.value = true
};
isAllowedPrivacyPolicy.value = true;
showAddGoodsModal.value = true;
}
async function onSetShelfClick(item: ResponsePointGoodModel, status: GoodsStatus) {
@@ -329,35 +329,35 @@ async function onSetShelfClick(item: ResponsePointGoodModel, status: GoodsStatus
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
d.loading = true
const originStatus = item.status
d.loading = true;
const originStatus = item.status;
try {
const { code, message: errMsg } = await QueryPostAPI(POINT_API_URL + 'update-goods-status', {
ids: [item.id],
status: status,
})
});
if (code === 200) {
message.success('成功')
const index = goods.value.findIndex(g => g.id === item.id)
message.success('成功');
const index = goods.value.findIndex(g => g.id === item.id);
if (index > -1) {
goods.value[index].status = status
goods.value[index].status = status;
}
} else {
message.error('失败: ' + errMsg)
item.status = originStatus
console.error(errMsg)
message.error('失败: ' + errMsg);
item.status = originStatus;
console.error(errMsg);
}
} catch (err) {
message.error('失败: ' + err)
item.status = originStatus
console.error(err)
message.error('失败: ' + err);
item.status = originStatus;
console.error(err);
} finally {
d.loading = false
d.loading = false;
}
},
})
});
}
function onDeleteClick(item: ResponsePointGoodModel) {
@@ -367,43 +367,43 @@ function onDeleteClick(item: ResponsePointGoodModel) {
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
d.loading = true
d.loading = true;
try {
const { code, message: errMsg } = await QueryGetAPI(POINT_API_URL + 'delete-goods', {
id: item.id,
})
});
if (code === 200) {
message.success('成功')
goods.value = goods.value.filter(g => g.id !== item.id)
message.success('成功');
goods.value = goods.value.filter(g => g.id !== item.id);
} else {
message.error('失败: ' + errMsg)
console.error(errMsg)
message.error('失败: ' + errMsg);
console.error(errMsg);
}
} catch (err) {
message.error('失败: ' + err)
console.error(err)
message.error('失败: ' + err);
console.error(err);
} finally {
d.loading = false
d.loading = false;
}
},
})
});
}
function onModalOpen() {
if (!currentGoodsModel.value.goods.id) {
resetGoods()
resetGoods();
}
showAddGoodsModal.value = true
showAddGoodsModal.value = true;
}
function resetGoods() {
currentGoodsModel.value = defaultGoodsModel()
isAllowedPrivacyPolicy.value = false
currentGoodsModel.value = defaultGoodsModel();
isAllowedPrivacyPolicy.value = false;
}
onMounted(() => { })
onMounted(() => { });
</script>
<template>
@@ -543,9 +543,7 @@ onMounted(() => { })
class="point-goods-card"
>
<template #footer>
<NFlex
:gap="8"
>
<NFlex :gap="8">
<NButton
type="info"
size="small"
@@ -882,7 +880,7 @@ onMounted(() => { })
@update:checked="
(v) => {
// @ts-ignore
currentGoodsModel.goods.setting.guardFree = v ? { year: undefined, month: undefined } : undefined
currentGoodsModel.goods.setting.guardFree = v ? { year: undefined, month: undefined } : undefined;
}
"
>
@@ -1127,9 +1125,7 @@ onMounted(() => { })
<div class="scroll-shadow-bottom" />
</div>
<template #footer>
<NFlex
justify="center"
>
<NFlex justify="center">
<NButton
type="primary"
size="large"

View File

@@ -32,7 +32,7 @@ onMounted(async () => {
rpc.expose('status', () => {
return {
status: webFetcher.state,
type: webFetcher.client?.type,
type: webFetcher.webfetcherType,
roomId: webFetcher.client instanceof OpenLiveClient ?
webFetcher.client.roomAuthInfo?.anchor_info.room_id :
webFetcher.client instanceof DirectClient ?

View File

@@ -52,9 +52,9 @@ export const DefaultConfig = {} as ConfigType
export const Config = defineTemplateConfig([
{
name: '封面',
type: 'image',
imageLimit: 1,
key: 'cover',
type: 'file',
fileLimit: 1,
key: 'coverFile',
},
{
name: '测试',