vip修改
This commit is contained in:
parent
15fbfa95de
commit
2dcd19a3ff
@ -4,20 +4,14 @@ import { computed, reactive, ref, watch } from 'vue';
|
|||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
import { useAccessStore } from '@vben/stores';
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
import {
|
|
||||||
getResidentVipConfig,
|
|
||||||
pageResidentVipOrders,
|
|
||||||
pageResidentVipUserStates,
|
|
||||||
saveResidentVipConfig,
|
|
||||||
} from '#/api/legacy/resident-activity';
|
|
||||||
import { listSysOriginTypeList } from '#/api/legacy/props';
|
|
||||||
import { getAllowedSysOrigins } from '#/views/system/shared';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Drawer,
|
||||||
|
Empty,
|
||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
|
message,
|
||||||
Modal,
|
Modal,
|
||||||
Pagination,
|
Pagination,
|
||||||
Select,
|
Select,
|
||||||
@ -25,10 +19,21 @@ import {
|
|||||||
Spin,
|
Spin,
|
||||||
Switch,
|
Switch,
|
||||||
Table,
|
Table,
|
||||||
|
TabPane,
|
||||||
|
Tabs,
|
||||||
Tag,
|
Tag,
|
||||||
message,
|
Tooltip,
|
||||||
} from 'antdv-next';
|
} from 'antdv-next';
|
||||||
|
|
||||||
|
import { listSysOriginTypeList } from '#/api/legacy/props';
|
||||||
|
import {
|
||||||
|
getResidentVipConfig,
|
||||||
|
pageResidentVipOrders,
|
||||||
|
pageResidentVipUserStates,
|
||||||
|
saveResidentVipConfig,
|
||||||
|
} from '#/api/legacy/resident-activity';
|
||||||
|
import { getAllowedSysOrigins } from '#/views/system/shared';
|
||||||
|
|
||||||
defineOptions({ name: 'ResidentVipConfigPage' });
|
defineOptions({ name: 'ResidentVipConfigPage' });
|
||||||
|
|
||||||
const resourceFields = [
|
const resourceFields = [
|
||||||
@ -66,20 +71,13 @@ const levelColumns = [
|
|||||||
{ dataIndex: 'enabled', key: 'enabled', title: '启用', width: 90 },
|
{ dataIndex: 'enabled', key: 'enabled', title: '启用', width: 90 },
|
||||||
{ dataIndex: 'displayName', key: 'displayName', title: '名称', width: 160 },
|
{ dataIndex: 'displayName', key: 'displayName', title: '名称', width: 160 },
|
||||||
{ dataIndex: 'priceGold', key: 'priceGold', title: '30天价格', width: 150 },
|
{ dataIndex: 'priceGold', key: 'priceGold', title: '30天价格', width: 150 },
|
||||||
...resourceFields.map((item) => ({
|
{ dataIndex: 'resources', key: 'resources', title: '素材', width: 560 },
|
||||||
dataIndex: item.key,
|
|
||||||
key: item.key,
|
|
||||||
title: item.title,
|
|
||||||
width: 260,
|
|
||||||
})),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const stateColumns = [
|
const stateColumns = [
|
||||||
{ dataIndex: 'userId', key: 'userId', title: '用户ID', width: 160 },
|
{ dataIndex: 'userId', key: 'userId', title: '用户ID', width: 160 },
|
||||||
{ dataIndex: 'status', key: 'status', title: '状态', width: 100 },
|
{ dataIndex: 'status', key: 'status', title: '状态', width: 100 },
|
||||||
{ dataIndex: 'level', key: 'level', title: '等级', width: 100 },
|
{ dataIndex: 'level', key: 'level', title: '等级', width: 100 },
|
||||||
{ dataIndex: 'displayName', key: 'displayName', title: '名称', width: 140 },
|
|
||||||
{ dataIndex: 'resources', key: 'resources', title: '当前资源', width: 360 },
|
|
||||||
{ dataIndex: 'expireAt', key: 'expireAt', title: '到期时间', width: 180 },
|
{ dataIndex: 'expireAt', key: 'expireAt', title: '到期时间', width: 180 },
|
||||||
{ dataIndex: 'updateTime', key: 'updateTime', title: '更新时间', width: 180 },
|
{ dataIndex: 'updateTime', key: 'updateTime', title: '更新时间', width: 180 },
|
||||||
];
|
];
|
||||||
@ -93,7 +91,7 @@ const orderColumns = [
|
|||||||
{ dataIndex: 'status', key: 'status', title: '状态', width: 100 },
|
{ dataIndex: 'status', key: 'status', title: '状态', width: 100 },
|
||||||
{ dataIndex: 'expireAt', key: 'expireAt', title: '权益到期', width: 180 },
|
{ dataIndex: 'expireAt', key: 'expireAt', title: '权益到期', width: 180 },
|
||||||
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
|
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
|
||||||
{ dataIndex: 'errorMessage', key: 'errorMessage', title: '失败原因', width: 280 },
|
{ dataIndex: 'errorMessage', key: 'errorMessage', title: '失败原因', width: 90 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
@ -112,6 +110,12 @@ const stateList = ref<Array<Record<string, any>>>([]);
|
|||||||
const orderList = ref<Array<Record<string, any>>>([]);
|
const orderList = ref<Array<Record<string, any>>>([]);
|
||||||
const resourceRows = reactive<Record<string, Array<Record<string, any>>>>({});
|
const resourceRows = reactive<Record<string, Array<Record<string, any>>>>({});
|
||||||
const resourceLoading = reactive<Record<string, boolean>>({});
|
const resourceLoading = reactive<Record<string, boolean>>({});
|
||||||
|
const activeTab = ref<'orders' | 'states'>('states');
|
||||||
|
const vipConfigDrawerOpen = ref(false);
|
||||||
|
const resourceDrawerOpen = ref(false);
|
||||||
|
const activeResourceLevel = ref<null | Record<string, any>>(null);
|
||||||
|
const activeResourceFieldKey = ref<ResourceField['key']>('longBadge');
|
||||||
|
const resourceKeyword = ref('');
|
||||||
|
|
||||||
const form = reactive<Record<string, any>>({
|
const form = reactive<Record<string, any>>({
|
||||||
configured: false,
|
configured: false,
|
||||||
@ -135,22 +139,37 @@ const orderQuery = reactive<Record<string, any>>({
|
|||||||
userId: '',
|
userId: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const statusMeta = computed(() => {
|
const activeResourceField = computed<ResourceField>(
|
||||||
if (!form.configured) {
|
() => findResourceField(activeResourceFieldKey.value) || resourceFields[0],
|
||||||
return {
|
);
|
||||||
color: 'processing',
|
|
||||||
description: '当前系统还没有保存 VIP 配置。',
|
const activeResourceList = computed(() => {
|
||||||
text: '未配置',
|
const field = activeResourceField.value;
|
||||||
};
|
const keyword = resourceKeyword.value.trim().toLowerCase();
|
||||||
|
const rows = resourceRows[field.key] || [];
|
||||||
|
if (!keyword) {
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
const enabledCount = (form.levels || []).filter((item: Record<string, any>) =>
|
return rows.filter((item) => {
|
||||||
Boolean(item.enabled),
|
return [
|
||||||
).length;
|
resourceOptionId(item),
|
||||||
return {
|
resourceOptionName(item),
|
||||||
color: enabledCount > 0 ? 'success' : 'default',
|
String(item.code || ''),
|
||||||
description: `已配置 5 个等级,当前启用 ${enabledCount} 个等级。`,
|
].some((value) => value.toLowerCase().includes(keyword));
|
||||||
text: enabledCount > 0 ? '已启用' : '已关闭',
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
|
const activeResource = computed(() => {
|
||||||
|
const record = activeResourceLevel.value;
|
||||||
|
if (!record) {
|
||||||
|
return createResource();
|
||||||
|
}
|
||||||
|
return normalizeResource(record[activeResourceField.value.key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeResourceTitle = computed(() => {
|
||||||
|
const level = activeResourceLevel.value?.level || '-';
|
||||||
|
return `VIP ${level} 素材配置`;
|
||||||
});
|
});
|
||||||
|
|
||||||
function createResource() {
|
function createResource() {
|
||||||
@ -182,7 +201,7 @@ function createLevel(level: number) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeResource(value: Record<string, any> | null | undefined) {
|
function normalizeResource(value: null | Record<string, any> | undefined) {
|
||||||
const resourceId = String(value?.resourceId ?? '').trim();
|
const resourceId = String(value?.resourceId ?? '').trim();
|
||||||
const url = String(value?.url || value?.sourceUrl || value?.resourceUrl || '').trim();
|
const url = String(value?.url || value?.sourceUrl || value?.resourceUrl || '').trim();
|
||||||
const cover = String(value?.cover || value?.coverUrl || value?.resourceCover || '').trim();
|
const cover = String(value?.cover || value?.coverUrl || value?.resourceCover || '').trim();
|
||||||
@ -197,7 +216,7 @@ function normalizeResource(value: Record<string, any> | null | undefined) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePropsSourceResource(value: Record<string, any> | null | undefined) {
|
function normalizePropsSourceResource(value: null | Record<string, any> | undefined) {
|
||||||
const resourceId = String(value?.id ?? value?.resourceId ?? '').trim();
|
const resourceId = String(value?.id ?? value?.resourceId ?? '').trim();
|
||||||
const cover = String(
|
const cover = String(
|
||||||
value?.cover || value?.coverUrl || value?.resourceCover || value?.selectUrl || '',
|
value?.cover || value?.coverUrl || value?.resourceCover || value?.selectUrl || '',
|
||||||
@ -232,50 +251,7 @@ function resetResourceRows() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resourceSelectValue(record: Record<string, any>, key: string) {
|
function buildResourceSavePayload(value: null | Record<string, any> | undefined) {
|
||||||
const resource = normalizeResource(record[key]);
|
|
||||||
return resource.resourceId || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resourceSelectOptions(record: Record<string, any>, key: string) {
|
|
||||||
const list = resourceRows[key] || [];
|
|
||||||
const options = list.map((item) => {
|
|
||||||
const code = String(item.code || '').trim();
|
|
||||||
const name = String(item.name || '').trim();
|
|
||||||
const id = String(item.id || '').trim();
|
|
||||||
return {
|
|
||||||
label: [id, name, code ? `(${code})` : ''].filter(Boolean).join(' / '),
|
|
||||||
value: id as any,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const current = normalizeResource(record[key]);
|
|
||||||
if (
|
|
||||||
current.resourceId &&
|
|
||||||
!options.some((item) => String(item.value) === String(current.resourceId))
|
|
||||||
) {
|
|
||||||
options.unshift({
|
|
||||||
label: `${current.resourceId} / ${current.name || '已保存资源'}`,
|
|
||||||
value: current.resourceId as any,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resourceMeta(record: Record<string, any>, key: string) {
|
|
||||||
const resource = normalizeResource(record[key]);
|
|
||||||
if (!resource.resourceId) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
resource.name,
|
|
||||||
resource.cover ? `封面: ${resource.cover}` : '',
|
|
||||||
resource.url ? `资源: ${resource.url}` : '',
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' / ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildResourceSavePayload(value: Record<string, any> | null | undefined) {
|
|
||||||
const resource = normalizeResource(value);
|
const resource = normalizeResource(value);
|
||||||
return {
|
return {
|
||||||
resourceId: resource.resourceId || '0',
|
resourceId: resource.resourceId || '0',
|
||||||
@ -328,37 +304,10 @@ function parseJsonObject(value: unknown) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadResourceOptionsByKey(key: string) {
|
|
||||||
const field = findResourceField(key);
|
|
||||||
if (field) {
|
|
||||||
await loadResourceOptions(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAllResourceOptions() {
|
async function loadAllResourceOptions() {
|
||||||
await Promise.all(resourceFields.map((field) => loadResourceOptions(field)));
|
await Promise.all(resourceFields.map((field) => loadResourceOptions(field)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleResourceChange(record: Record<string, any>, key: string, value?: number | string) {
|
|
||||||
const field = findResourceField(key);
|
|
||||||
if (!field) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!value) {
|
|
||||||
record[field.key] = createResource();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const selected = (resourceRows[field.key] || []).find(
|
|
||||||
(item) => String(item.id) === String(value),
|
|
||||||
);
|
|
||||||
record[field.key] = selected
|
|
||||||
? normalizePropsSourceResource(selected)
|
|
||||||
: {
|
|
||||||
...normalizeResource(record[field.key]),
|
|
||||||
resourceId: String(value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeLevels(levels: Array<Record<string, any>> | undefined) {
|
function normalizeLevels(levels: Array<Record<string, any>> | undefined) {
|
||||||
const levelMap = new Map<number, Record<string, any>>();
|
const levelMap = new Map<number, Record<string, any>>();
|
||||||
for (const item of levels || []) {
|
for (const item of levels || []) {
|
||||||
@ -375,7 +324,7 @@ function normalizeLevels(levels: Array<Record<string, any>> | undefined) {
|
|||||||
displayName: String(item.displayName || `VIP ${level}`),
|
displayName: String(item.displayName || `VIP ${level}`),
|
||||||
enabled: Boolean(item.enabled),
|
enabled: Boolean(item.enabled),
|
||||||
priceGold: Number(item.priceGold || 0),
|
priceGold: Number(item.priceGold || 0),
|
||||||
longBadge: normalizeResource(item.longBadge || item['badge']),
|
longBadge: normalizeResource(item.longBadge || item.badge),
|
||||||
shortBadge: normalizeResource(item.shortBadge),
|
shortBadge: normalizeResource(item.shortBadge),
|
||||||
avatarFrame: normalizeResource(item.avatarFrame),
|
avatarFrame: normalizeResource(item.avatarFrame),
|
||||||
entryEffect: normalizeResource(item.entryEffect),
|
entryEffect: normalizeResource(item.entryEffect),
|
||||||
@ -387,7 +336,7 @@ function normalizeLevels(levels: Array<Record<string, any>> | undefined) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyConfig(result: Record<string, any> | null | undefined) {
|
function applyConfig(result: null | Record<string, any> | undefined) {
|
||||||
const value = result ?? {};
|
const value = result ?? {};
|
||||||
form.configured = Boolean(value.configured);
|
form.configured = Boolean(value.configured);
|
||||||
form.sysOrigin = String(value.sysOrigin || form.sysOrigin || sysOriginOptions.value[0]?.value || '');
|
form.sysOrigin = String(value.sysOrigin || form.sysOrigin || sysOriginOptions.value[0]?.value || '');
|
||||||
@ -523,30 +472,137 @@ function handleOrderPageChange(page: number, pageSize: number) {
|
|||||||
function statusColor(status: string) {
|
function statusColor(status: string) {
|
||||||
switch (String(status || '').toUpperCase()) {
|
switch (String(status || '').toUpperCase()) {
|
||||||
case 'ACTIVE':
|
case 'ACTIVE':
|
||||||
case 'SUCCESS':
|
case 'SUCCESS': {
|
||||||
return 'success';
|
return 'success';
|
||||||
case 'FAILED':
|
}
|
||||||
return 'error';
|
case 'EXPIRED': {
|
||||||
case 'EXPIRED':
|
|
||||||
return 'default';
|
return 'default';
|
||||||
default:
|
}
|
||||||
|
case 'FAILED': {
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
return 'processing';
|
return 'processing';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function orderTypeText(type: string) {
|
function orderTypeText(type: string) {
|
||||||
const match = orderTypeOptions.find((item) => item.value === String(type || '').toUpperCase());
|
const match = orderTypeOptions.find((item) => item.value === String(type || '').toUpperCase());
|
||||||
return match?.label || type || '-';
|
return match?.label || type || '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
function resourceSummary(record: Record<string, any>) {
|
function errorMessageText(record: Record<string, any>) {
|
||||||
|
return String(record.errorMessage || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedResourceSummaries(record: Record<string, any>) {
|
||||||
return resourceFields
|
return resourceFields
|
||||||
.map((field) => {
|
.map((field) => {
|
||||||
const resource = record[field.key] || {};
|
const resource = normalizeResource(record[field.key]);
|
||||||
const text = String(resource.name || resource.resourceId || '').trim();
|
const name = String(resource.name || resource.resourceId || '').trim();
|
||||||
return text ? `${field.title}: ${text}` : '';
|
if (!name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
cover: resource.cover,
|
||||||
|
key: field.key,
|
||||||
|
name,
|
||||||
|
title: field.title,
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean) as Array<{
|
||||||
|
cover: string;
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedResourceCount(record: Record<string, any>) {
|
||||||
|
return selectedResourceSummaries(record).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceFieldSelected(record: Record<string, any>, field: ResourceField) {
|
||||||
|
return Boolean(normalizeResource(record[field.key]).resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceOptionId(item: Record<string, any>) {
|
||||||
|
return String(item.id ?? item.resourceId ?? item.sourceId ?? '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceOptionName(item: Record<string, any>) {
|
||||||
|
const id = resourceOptionId(item);
|
||||||
|
return String(
|
||||||
|
item.name ||
|
||||||
|
item.resourceName ||
|
||||||
|
item.sourceName ||
|
||||||
|
item.badgeName ||
|
||||||
|
item.title ||
|
||||||
|
(id ? `资源 ${id}` : ''),
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceOptionCover(item: Record<string, any>) {
|
||||||
|
return String(
|
||||||
|
item.cover ||
|
||||||
|
item.coverUrl ||
|
||||||
|
item.resourceCover ||
|
||||||
|
item.resourceCoverUrl ||
|
||||||
|
item.selectUrl ||
|
||||||
|
item.icon ||
|
||||||
|
item.sourceUrl ||
|
||||||
|
item.resourceUrl ||
|
||||||
|
item.animationUrl ||
|
||||||
|
'',
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceOptionMeta(item: Record<string, any>) {
|
||||||
|
return [resourceOptionId(item) ? `ID ${resourceOptionId(item)}` : '', item.code || '']
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' / ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openResourceDrawer(record: Record<string, any>) {
|
||||||
|
activeResourceLevel.value = record;
|
||||||
|
activeResourceFieldKey.value =
|
||||||
|
resourceFields.find((field) => !resourceFieldSelected(record, field))?.key ||
|
||||||
|
resourceFields[0].key;
|
||||||
|
resourceKeyword.value = '';
|
||||||
|
resourceDrawerOpen.value = true;
|
||||||
|
void loadAllResourceOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeResourceDrawer() {
|
||||||
|
resourceDrawerOpen.value = false;
|
||||||
|
activeResourceLevel.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectResourceField(field: ResourceField) {
|
||||||
|
activeResourceFieldKey.value = field.key;
|
||||||
|
resourceKeyword.value = '';
|
||||||
|
void loadResourceOptions(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectResource(item: Record<string, any>) {
|
||||||
|
const record = activeResourceLevel.value;
|
||||||
|
if (!record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
record[activeResourceField.value.key] = normalizePropsSourceResource(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearActiveResource() {
|
||||||
|
const record = activeResourceLevel.value;
|
||||||
|
if (!record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
record[activeResourceField.value.key] = createResource();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActiveResource(item: Record<string, any>) {
|
||||||
|
return String(activeResource.value.resourceId) === resourceOptionId(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -576,89 +632,10 @@ watch(
|
|||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
<Spin :spinning="loading">
|
<Spin :spinning="loading">
|
||||||
<Card :bordered="false" title="VIP配置">
|
<Card :bordered="false" title="VIP管理">
|
||||||
<Space class="toolbar" wrap>
|
<div class="tabs-head">
|
||||||
<Select
|
<Tabs v-model:active-key="activeTab" class="vip-tabs">
|
||||||
v-model:value="form.sysOrigin"
|
<TabPane key="states" tab="用户VIP状态">
|
||||||
:options="sysOriginOptions"
|
|
||||||
option-filter-prop="label"
|
|
||||||
option-label-prop="label"
|
|
||||||
placeholder="请选择系统"
|
|
||||||
show-search
|
|
||||||
style="width: 180px"
|
|
||||||
/>
|
|
||||||
<Tag :color="statusMeta.color">
|
|
||||||
{{ statusMeta.text }}
|
|
||||||
</Tag>
|
|
||||||
<span class="status-desc">{{ statusMeta.description }}</span>
|
|
||||||
</Space>
|
|
||||||
|
|
||||||
<Table
|
|
||||||
:columns="levelColumns"
|
|
||||||
:data-source="form.levels"
|
|
||||||
:pagination="false"
|
|
||||||
row-key="level"
|
|
||||||
:scroll="{ x: 2600 }"
|
|
||||||
>
|
|
||||||
<template #bodyCell="{ column, record }">
|
|
||||||
<template v-if="column.key === 'level'">
|
|
||||||
<Tag color="gold">VIP {{ record.level }}</Tag>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.key === 'enabled'">
|
|
||||||
<Switch v-model:checked="record.enabled" />
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.key === 'displayName'">
|
|
||||||
<Input v-model:value="record.displayName" placeholder="等级名称" />
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.key === 'priceGold'">
|
|
||||||
<InputNumber
|
|
||||||
v-model:value="record.priceGold"
|
|
||||||
:min="0"
|
|
||||||
:precision="0"
|
|
||||||
addon-after="金币"
|
|
||||||
style="width: 130px"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="resourceFields.some((item) => item.key === String(column.key))">
|
|
||||||
<div class="resource-editor">
|
|
||||||
<Select
|
|
||||||
:value="resourceSelectValue(record, String(column.key))"
|
|
||||||
allow-clear
|
|
||||||
:loading="resourceLoading[String(column.key)]"
|
|
||||||
:not-found-content="resourceLoading[String(column.key)] ? undefined : '暂无资源'"
|
|
||||||
option-filter-prop="label"
|
|
||||||
option-label-prop="label"
|
|
||||||
:options="resourceSelectOptions(record, String(column.key))"
|
|
||||||
placeholder="请选择资源"
|
|
||||||
show-search
|
|
||||||
style="width: 100%"
|
|
||||||
@change="(value) => handleResourceChange(record, String(column.key), value)"
|
|
||||||
@focus="loadResourceOptionsByKey(String(column.key))"
|
|
||||||
/>
|
|
||||||
<div v-if="resourceMeta(record, String(column.key))" class="resource-meta">
|
|
||||||
{{ resourceMeta(record, String(column.key)) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<div class="tips">
|
|
||||||
<div>固定 5 个等级,每次购买 30 天;同等级再次购买会续 30 天。</div>
|
|
||||||
<div>资源从道具资源配置选择,保存时会记录当前资源快照。</div>
|
|
||||||
<div>用户只能升级,升级时支付目标等级和当前等级的差价,到期后资源自动失效。</div>
|
|
||||||
<div>升级成功后会把长徽章、短徽章、头像框、进场特效、聊天气泡、飘窗和背景卡替换成新等级资源;动效图只用于 VIP 页面展示。</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Space class="actions" wrap>
|
|
||||||
<Button :loading="saving" type="primary" @click="handleSave">
|
|
||||||
保存配置
|
|
||||||
</Button>
|
|
||||||
<span v-if="form.updateTime" class="field-desc">最后更新时间:{{ form.updateTime }}</span>
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card :bordered="false" class="record-card" title="用户VIP状态">
|
|
||||||
<Space class="toolbar" wrap>
|
<Space class="toolbar" wrap>
|
||||||
<Input
|
<Input
|
||||||
v-model:value="stateQuery.userId"
|
v-model:value="stateQuery.userId"
|
||||||
@ -684,7 +661,7 @@ watch(
|
|||||||
:loading="stateLoading"
|
:loading="stateLoading"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
row-key="userId"
|
row-key="userId"
|
||||||
:scroll="{ x: 1220 }"
|
:scroll="{ x: 760 }"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'status'">
|
<template v-if="column.key === 'status'">
|
||||||
@ -696,14 +673,6 @@ watch(
|
|||||||
<Tag v-if="record.active" color="gold">VIP {{ record.level }}</Tag>
|
<Tag v-if="record.active" color="gold">VIP {{ record.level }}</Tag>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'resources'">
|
|
||||||
<div v-if="resourceSummary(record).length > 0" class="resource-summary">
|
|
||||||
<div v-for="item in resourceSummary(record)" :key="item">
|
|
||||||
{{ item }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
@ -714,12 +683,12 @@ watch(
|
|||||||
:total="stateTotal"
|
:total="stateTotal"
|
||||||
show-size-changer
|
show-size-changer
|
||||||
@change="handleStatePageChange"
|
@change="handleStatePageChange"
|
||||||
@showSizeChange="handleStatePageChange"
|
@show-size-change="handleStatePageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</TabPane>
|
||||||
|
|
||||||
<Card :bordered="false" class="record-card" title="VIP订单记录">
|
<TabPane key="orders" tab="VIP订单记录">
|
||||||
<Space class="toolbar" wrap>
|
<Space class="toolbar" wrap>
|
||||||
<Input
|
<Input
|
||||||
v-model:value="orderQuery.userId"
|
v-model:value="orderQuery.userId"
|
||||||
@ -767,7 +736,10 @@ watch(
|
|||||||
</Tag>
|
</Tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'errorMessage'">
|
<template v-else-if="column.key === 'errorMessage'">
|
||||||
{{ record.errorMessage || '-' }}
|
<Tooltip v-if="errorMessageText(record)" :title="errorMessageText(record)">
|
||||||
|
<span class="failure-icon">!</span>
|
||||||
|
</Tooltip>
|
||||||
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
@ -779,10 +751,175 @@ watch(
|
|||||||
:total="orderTotal"
|
:total="orderTotal"
|
||||||
show-size-changer
|
show-size-changer
|
||||||
@change="handleOrderPageChange"
|
@change="handleOrderPageChange"
|
||||||
@showSizeChange="handleOrderPageChange"
|
@show-size-change="handleOrderPageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Button class="config-entry" @click="vipConfigDrawerOpen = true">
|
||||||
|
VIP配置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
:open="vipConfigDrawerOpen"
|
||||||
|
destroy-on-close
|
||||||
|
title="VIP配置"
|
||||||
|
width="1180"
|
||||||
|
@close="vipConfigDrawerOpen = false"
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
:columns="levelColumns"
|
||||||
|
:data-source="form.levels"
|
||||||
|
:pagination="false"
|
||||||
|
row-key="level"
|
||||||
|
:scroll="{ x: 1050 }"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'level'">
|
||||||
|
<Tag color="gold">VIP {{ record.level }}</Tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'enabled'">
|
||||||
|
<Switch v-model:checked="record.enabled" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'displayName'">
|
||||||
|
<Input v-model:value="record.displayName" placeholder="等级名称" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'priceGold'">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="record.priceGold"
|
||||||
|
:min="0"
|
||||||
|
:precision="0"
|
||||||
|
addon-after="金币"
|
||||||
|
style="width: 130px"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'resources'">
|
||||||
|
<div class="resource-cell">
|
||||||
|
<div class="resource-cell-main">
|
||||||
|
<div v-if="selectedResourceSummaries(record).length > 0" class="resource-chip-list">
|
||||||
|
<span
|
||||||
|
v-for="item in selectedResourceSummaries(record)"
|
||||||
|
:key="item.key"
|
||||||
|
class="resource-chip"
|
||||||
|
>
|
||||||
|
<img v-if="item.cover" :src="item.cover" alt="" class="resource-chip-cover" />
|
||||||
|
<span class="resource-chip-text">{{ item.title }}: {{ item.name }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span v-else class="empty-resource">未选择素材</span>
|
||||||
|
<div class="resource-cell-count">
|
||||||
|
已选 {{ selectedResourceCount(record) }} / {{ resourceFields.length }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button size="small" type="primary" @click="openResourceDrawer(record)">
|
||||||
|
配置素材
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<div class="tips">
|
||||||
|
<div>固定 5 个等级,每次购买 30 天;同等级再次购买会续 30 天。</div>
|
||||||
|
<div>资源从道具资源配置选择,保存时会记录当前资源快照。</div>
|
||||||
|
<div>用户只能升级,升级时支付目标等级和当前等级的差价,到期后资源自动失效。</div>
|
||||||
|
<div>升级成功后会把长徽章、短徽章、头像框、进场特效、聊天气泡、飘窗和背景卡替换成新等级资源;动效图只用于 VIP 页面展示。</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space class="actions" wrap>
|
||||||
|
<Button :loading="saving" type="primary" @click="handleSave">
|
||||||
|
保存配置
|
||||||
|
</Button>
|
||||||
|
<span v-if="form.updateTime" class="field-desc">最后更新时间:{{ form.updateTime }}</span>
|
||||||
|
</Space>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
:open="resourceDrawerOpen"
|
||||||
|
destroy-on-close
|
||||||
|
:title="activeResourceTitle"
|
||||||
|
width="960"
|
||||||
|
@close="closeResourceDrawer"
|
||||||
|
>
|
||||||
|
<div class="resource-drawer">
|
||||||
|
<aside class="resource-field-list">
|
||||||
|
<button
|
||||||
|
v-for="field in resourceFields"
|
||||||
|
:key="field.key"
|
||||||
|
class="resource-field-item"
|
||||||
|
:class="{
|
||||||
|
'is-active': activeResourceField.key === field.key,
|
||||||
|
'is-selected': activeResourceLevel && resourceFieldSelected(activeResourceLevel, field),
|
||||||
|
}"
|
||||||
|
type="button"
|
||||||
|
@click="selectResourceField(field)"
|
||||||
|
>
|
||||||
|
<span>{{ field.title }}</span>
|
||||||
|
<Tag
|
||||||
|
v-if="activeResourceLevel && resourceFieldSelected(activeResourceLevel, field)"
|
||||||
|
color="success"
|
||||||
|
>
|
||||||
|
已选
|
||||||
|
</Tag>
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="resource-picker">
|
||||||
|
<div class="resource-picker-head">
|
||||||
|
<div>
|
||||||
|
<div class="resource-picker-title">{{ activeResourceField.title }}</div>
|
||||||
|
<div class="resource-picker-subtitle">
|
||||||
|
当前:{{ activeResource.name || activeResource.resourceId || '未选择' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Space>
|
||||||
|
<Input
|
||||||
|
v-model:value="resourceKeyword"
|
||||||
|
allow-clear
|
||||||
|
placeholder="资源名称 / ID"
|
||||||
|
style="width: 240px"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
:disabled="!activeResource.resourceId"
|
||||||
|
size="small"
|
||||||
|
@click="clearActiveResource"
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spin :spinning="resourceLoading[activeResourceField.key]">
|
||||||
|
<div v-if="activeResourceList.length > 0" class="resource-grid">
|
||||||
|
<button
|
||||||
|
v-for="item in activeResourceList"
|
||||||
|
:key="`${activeResourceField.key}-${resourceOptionId(item)}`"
|
||||||
|
class="resource-card"
|
||||||
|
:class="{ 'is-active': isActiveResource(item) }"
|
||||||
|
type="button"
|
||||||
|
@click="selectResource(item)"
|
||||||
|
>
|
||||||
|
<div class="resource-cover-wrap">
|
||||||
|
<img
|
||||||
|
v-if="resourceOptionCover(item)"
|
||||||
|
:src="resourceOptionCover(item)"
|
||||||
|
alt=""
|
||||||
|
class="resource-cover"
|
||||||
|
/>
|
||||||
|
<span v-else class="resource-cover-empty">无图</span>
|
||||||
|
</div>
|
||||||
|
<div class="resource-name">{{ resourceOptionName(item) || '-' }}</div>
|
||||||
|
<div class="resource-meta">{{ resourceOptionMeta(item) || '-' }}</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Empty v-else description="暂无可选资源" />
|
||||||
|
</Spin>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
</Spin>
|
</Spin>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
@ -792,25 +929,25 @@ watch(
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-desc,
|
.tabs-head {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vip-tabs {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-entry {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.field-desc {
|
.field-desc {
|
||||||
color: rgb(100 116 139);
|
color: rgb(100 116 139);
|
||||||
}
|
}
|
||||||
|
|
||||||
.resource-editor {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-meta {
|
|
||||||
color: rgb(100 116 139);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.4;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tips {
|
.tips {
|
||||||
background: rgb(248 250 252);
|
background: rgb(248 250 252);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -824,13 +961,217 @@ watch(
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-card {
|
.resource-cell {
|
||||||
margin-top: 16px;
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resource-summary {
|
.resource-cell-main {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-chip-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
max-height: 58px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-chip {
|
||||||
|
align-items: center;
|
||||||
|
background: rgb(248 250 252);
|
||||||
|
border: 1px solid rgb(226 232 240);
|
||||||
|
border-radius: 6px;
|
||||||
color: rgb(51 65 85);
|
color: rgb(51 65 85);
|
||||||
line-height: 1.8;
|
display: inline-flex;
|
||||||
|
gap: 5px;
|
||||||
|
max-width: 180px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-chip-cover {
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 18px;
|
||||||
|
object-fit: cover;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-chip-text {
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-cell-count,
|
||||||
|
.empty-resource {
|
||||||
|
color: rgb(100 116 139);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-cell-count {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failure-icon {
|
||||||
|
align-items: center;
|
||||||
|
background: rgb(254 226 226);
|
||||||
|
border: 1px solid rgb(248 113 113);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: rgb(220 38 38);
|
||||||
|
cursor: help;
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
height: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-drawer {
|
||||||
|
align-items: start;
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
grid-template-columns: 180px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-field-list {
|
||||||
|
align-self: start;
|
||||||
|
border-right: 1px solid rgb(226 232 240);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-field-item {
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgb(226 232 240);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: rgb(51 65 85);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 0 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-field-item.is-active {
|
||||||
|
background: rgb(239 246 255);
|
||||||
|
border-color: rgb(37 99 235);
|
||||||
|
color: rgb(29 78 216);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-field-item.is-selected:not(.is-active) {
|
||||||
|
border-color: rgb(34 197 94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-picker {
|
||||||
|
max-height: calc(100vh - 160px);
|
||||||
|
min-width: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-picker-head {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-picker-title {
|
||||||
|
color: rgb(15 23 42);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-picker-subtitle {
|
||||||
|
color: rgb(100 116 139);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
max-width: 360px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgb(226 232 240);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 160px;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
transition:
|
||||||
|
border-color 0.2s ease,
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-card:hover,
|
||||||
|
.resource-card.is-active {
|
||||||
|
border-color: rgb(37 99 235);
|
||||||
|
box-shadow: 0 12px 24px rgb(37 99 235 / 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-cover-wrap {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 88px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-cover {
|
||||||
|
border-radius: 8px;
|
||||||
|
max-height: 88px;
|
||||||
|
max-width: 104px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-cover-empty {
|
||||||
|
align-items: center;
|
||||||
|
background: rgb(241 245 249);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: rgb(100 116 139);
|
||||||
|
display: inline-flex;
|
||||||
|
height: 72px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-name {
|
||||||
|
color: rgb(15 23 42);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
min-height: 40px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-meta {
|
||||||
|
color: rgb(100 116 139);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 6px;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pager {
|
.pager {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user