Compare commits
2 Commits
abbf6d9556
...
bd3dde8bac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd3dde8bac | ||
|
|
d0c917f30f |
268
apps/src/views/_shared/admin-config-table.vue
Normal file
268
apps/src/views/_shared/admin-config-table.vue
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Pagination, Table } from 'antdv-next';
|
||||||
|
|
||||||
|
defineOptions({ name: 'AdminConfigTable' });
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
autoHeight?: boolean;
|
||||||
|
columns: Array<Record<string, any>>;
|
||||||
|
current: number;
|
||||||
|
dataSource: Array<Record<string, any>>;
|
||||||
|
loading?: boolean;
|
||||||
|
pageSize: number;
|
||||||
|
plain?: boolean;
|
||||||
|
rowKey?: string;
|
||||||
|
scrollX?: number | string;
|
||||||
|
showSizeChanger?: boolean;
|
||||||
|
total: number;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
autoHeight: false,
|
||||||
|
loading: false,
|
||||||
|
plain: false,
|
||||||
|
rowKey: 'id',
|
||||||
|
scrollX: 1000,
|
||||||
|
showSizeChanger: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pageChange: [page: number, pageSize: number];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const tableAreaRef = ref<HTMLDivElement | null>(null);
|
||||||
|
const pagerRef = ref<HTMLDivElement | null>(null);
|
||||||
|
const tableScrollY = ref(320);
|
||||||
|
let tableResizeObserver: null | ResizeObserver = null;
|
||||||
|
|
||||||
|
const tableScroll = computed(() => {
|
||||||
|
const scroll: Record<string, any> = { x: props.scrollX };
|
||||||
|
if (props.autoHeight) {
|
||||||
|
scroll.y = tableScrollY.value;
|
||||||
|
}
|
||||||
|
return scroll;
|
||||||
|
});
|
||||||
|
|
||||||
|
function stringifyCellValue(value: any) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumnValue(record: Record<string, any>, column: Record<string, any>) {
|
||||||
|
const dataIndex = column?.dataIndex;
|
||||||
|
if (Array.isArray(dataIndex)) {
|
||||||
|
let value = record;
|
||||||
|
for (const key of dataIndex) {
|
||||||
|
value = value?.[String(key)];
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (dataIndex === undefined || dataIndex === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return record?.[String(dataIndex)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTableScrollY() {
|
||||||
|
if (!props.autoHeight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const area = tableAreaRef.value;
|
||||||
|
if (!area) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const headerHeight =
|
||||||
|
area.querySelector('.ant-table-thead')?.getBoundingClientRect().height || 48;
|
||||||
|
const scrollbarReserve = 8;
|
||||||
|
tableScrollY.value = Math.max(
|
||||||
|
160,
|
||||||
|
Math.floor(area.clientHeight - headerHeight - scrollbarReserve),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshTableScroll() {
|
||||||
|
await nextTick();
|
||||||
|
updateTableScrollY();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await refreshTableScroll();
|
||||||
|
if (!props.autoHeight || typeof ResizeObserver === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tableResizeObserver = new ResizeObserver(() => {
|
||||||
|
void refreshTableScroll();
|
||||||
|
});
|
||||||
|
if (tableAreaRef.value) {
|
||||||
|
tableResizeObserver.observe(tableAreaRef.value);
|
||||||
|
}
|
||||||
|
if (pagerRef.value) {
|
||||||
|
tableResizeObserver.observe(pagerRef.value);
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', updateTableScrollY);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
tableResizeObserver?.disconnect();
|
||||||
|
window.removeEventListener('resize', updateTableScrollY);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="admin-config-table-card"
|
||||||
|
:class="{ 'admin-config-table-card--plain': plain }"
|
||||||
|
>
|
||||||
|
<div ref="tableAreaRef" class="admin-config-table-area">
|
||||||
|
<Table
|
||||||
|
class="admin-config-table"
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="dataSource"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="false"
|
||||||
|
:row-key="rowKey"
|
||||||
|
:scroll="tableScroll"
|
||||||
|
>
|
||||||
|
<template #headerCell="slotProps">
|
||||||
|
<slot name="headerCell" v-bind="slotProps">
|
||||||
|
{{ slotProps.column.title }}
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
<template #bodyCell="slotProps">
|
||||||
|
<slot name="bodyCell" v-bind="slotProps">
|
||||||
|
{{ stringifyCellValue(getColumnValue(slotProps.record, slotProps.column)) }}
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref="pagerRef" class="admin-config-table-pager">
|
||||||
|
<Pagination
|
||||||
|
:current="current"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:total="total"
|
||||||
|
:show-size-changer="showSizeChanger"
|
||||||
|
@change="(page: number, size: number) => emit('pageChange', page, size)"
|
||||||
|
@show-size-change="(page: number, size: number) => emit('pageChange', page, size)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-config-table-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgb(229 231 235);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table-card--plain {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table-area {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table :deep(.ant-table) {
|
||||||
|
color: rgb(52 64 84);
|
||||||
|
font-size: var(--density-table-body-font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table :deep(.ant-spin-nested-loading),
|
||||||
|
.admin-config-table :deep(.ant-spin-container) {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table :deep(.ant-spin-container) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table :deep(.ant-table) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table :deep(.ant-table-container) {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table :deep(.ant-table-thead > tr > th) {
|
||||||
|
background: #fafafa;
|
||||||
|
border-bottom-color: rgb(237 240 244);
|
||||||
|
color: rgb(71 84 103);
|
||||||
|
font-size: var(--density-table-header-font-size);
|
||||||
|
font-weight: 500;
|
||||||
|
height: var(--density-table-header-height);
|
||||||
|
line-height: 1.35;
|
||||||
|
padding: 8px var(--density-table-cell-padding-x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table :deep(.ant-table-thead > tr > th::before) {
|
||||||
|
background-color: rgb(237 240 244) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table :deep(.ant-table-tbody > tr > td) {
|
||||||
|
border-bottom-color: rgb(237 240 244);
|
||||||
|
height: var(--density-table-row-height);
|
||||||
|
line-height: 1.35;
|
||||||
|
padding: 3px var(--density-table-cell-padding-x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table :deep(.ant-table-body) {
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table :deep(.switch-column) {
|
||||||
|
padding-left: var(--density-table-fixed-cell-padding-x) !important;
|
||||||
|
padding-right: var(--density-table-fixed-cell-padding-x) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table :deep(.action-column) {
|
||||||
|
padding-left: var(--density-table-fixed-cell-padding-x) !important;
|
||||||
|
padding-right: var(--density-table-fixed-cell-padding-x) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table :deep(.ant-table-tbody > tr:hover > td) {
|
||||||
|
background: rgb(250 252 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table :deep(.ant-table-cell-fix-left),
|
||||||
|
.admin-config-table :deep(.ant-table-cell-fix-right) {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table :deep(.ant-table-tbody > tr:hover > .ant-table-cell-fix-left),
|
||||||
|
.admin-config-table :deep(.ant-table-tbody > tr:hover > .ant-table-cell-fix-right) {
|
||||||
|
background: rgb(250 252 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-config-table-pager {
|
||||||
|
border-top: 1px solid rgb(237 240 244);
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
104
apps/src/views/_shared/resource-code.ts
Normal file
104
apps/src/views/_shared/resource-code.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
const RESOURCE_NAME_TRANSLATION_ENTRIES = ([
|
||||||
|
['头像框', 'avatar frame'],
|
||||||
|
['座驾', 'ride'],
|
||||||
|
['坐骑', 'mount'],
|
||||||
|
['勋章', 'badge'],
|
||||||
|
['徽章', 'badge'],
|
||||||
|
['管理员', 'admin'],
|
||||||
|
['圣诞', 'christmas'],
|
||||||
|
['新年', 'new year'],
|
||||||
|
['情人节', 'valentine'],
|
||||||
|
['万圣节', 'halloween'],
|
||||||
|
['锦鲤', 'koi'],
|
||||||
|
['熊猫', 'panda'],
|
||||||
|
['独角兽', 'unicorn'],
|
||||||
|
['天使', 'angel'],
|
||||||
|
['恶魔', 'devil'],
|
||||||
|
['皇冠', 'crown'],
|
||||||
|
['国王', 'king'],
|
||||||
|
['女王', 'queen'],
|
||||||
|
['玫瑰', 'rose'],
|
||||||
|
['星光', 'starlight'],
|
||||||
|
['星星', 'star'],
|
||||||
|
['月亮', 'moon'],
|
||||||
|
['太阳', 'sun'],
|
||||||
|
['彩虹', 'rainbow'],
|
||||||
|
['火焰', 'flame'],
|
||||||
|
['冰雪', 'snow'],
|
||||||
|
['蓝色', 'blue'],
|
||||||
|
['红色', 'red'],
|
||||||
|
['紫色', 'purple'],
|
||||||
|
['粉色', 'pink'],
|
||||||
|
['金色', 'gold'],
|
||||||
|
['银色', 'silver'],
|
||||||
|
['黑色', 'black'],
|
||||||
|
['白色', 'white'],
|
||||||
|
['绿色', 'green'],
|
||||||
|
['黄色', 'yellow'],
|
||||||
|
['钻石', 'diamond'],
|
||||||
|
['水晶', 'crystal'],
|
||||||
|
['幸运', 'lucky'],
|
||||||
|
['浪漫', 'romantic'],
|
||||||
|
['荣耀', 'glory'],
|
||||||
|
['守护', 'guard'],
|
||||||
|
['贵族', 'noble'],
|
||||||
|
['梦幻', 'dream'],
|
||||||
|
['闪耀', 'shining'],
|
||||||
|
['限定', 'limited'],
|
||||||
|
['豪华', 'deluxe'],
|
||||||
|
['高级', 'premium'],
|
||||||
|
['经典', 'classic'],
|
||||||
|
['热气球', 'hot air balloon'],
|
||||||
|
['跑车', 'sports car'],
|
||||||
|
['飞船', 'spaceship'],
|
||||||
|
['游艇', 'yacht'],
|
||||||
|
['摩托', 'motorcycle'],
|
||||||
|
['龙', 'dragon'],
|
||||||
|
['凤', 'phoenix'],
|
||||||
|
['虎', 'tiger'],
|
||||||
|
['兔', 'rabbit'],
|
||||||
|
['猫', 'cat'],
|
||||||
|
['狗', 'dog'],
|
||||||
|
['鱼', 'fish'],
|
||||||
|
] as Array<[string, string]>).toSorted(
|
||||||
|
(current, next) => next[0].length - current[0].length,
|
||||||
|
);
|
||||||
|
|
||||||
|
export function normalizeResourceBaseName(value: string) {
|
||||||
|
const name = value
|
||||||
|
.replace(/\.[^.]+$/, '')
|
||||||
|
.replace(/([_-]?(cover|covers|source|resource|resources|ios|android|expand))$/i, '')
|
||||||
|
.replace(/([_-]?(封面|资源))$/i, '')
|
||||||
|
.trim();
|
||||||
|
return name || value.replace(/\.[^.]+$/, '') || 'resource';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeResourceCode(value: string) {
|
||||||
|
const baseName = normalizeResourceBaseName(value);
|
||||||
|
const translated = translateResourceName(baseName);
|
||||||
|
return slugifyResourceCode(translated) || slugifyResourceCode(baseName) || 'resource';
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateResourceName(value: string) {
|
||||||
|
let result = value.trim();
|
||||||
|
RESOURCE_NAME_TRANSLATION_ENTRIES.forEach(([source, target]) => {
|
||||||
|
result = result.replaceAll(source, ` ${target} `);
|
||||||
|
});
|
||||||
|
return [...result]
|
||||||
|
.map((char) => {
|
||||||
|
if (/[\u4E00-\u9FFF]/.test(char)) {
|
||||||
|
return ` u${char.codePointAt(0)?.toString(16) || ''} `;
|
||||||
|
}
|
||||||
|
return char;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function slugifyResourceCode(value: string) {
|
||||||
|
return value
|
||||||
|
.normalize('NFKC')
|
||||||
|
.replaceAll(/[^\d A-Za-z]+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.replaceAll(/\s+/g, '_')
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
1227
apps/src/views/operate/components/gift-batch-drawer.vue
Normal file
1227
apps/src/views/operate/components/gift-batch-drawer.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -38,9 +38,10 @@ import {
|
|||||||
regionConfigTable,
|
regionConfigTable,
|
||||||
} from '#/api/legacy/system';
|
} from '#/api/legacy/system';
|
||||||
import AccountInput from '#/components/account-input.vue';
|
import AccountInput from '#/components/account-input.vue';
|
||||||
import { copyText } from '#/views/operate/shared';
|
import AdminConfigTable from '#/views/_shared/admin-config-table.vue';
|
||||||
import { formatDate, getAllowedSysOrigins } from '#/views/system/shared';
|
import { formatDate, getAllowedSysOrigins } from '#/views/system/shared';
|
||||||
|
|
||||||
|
import GiftBatchDrawer from './components/gift-batch-drawer.vue';
|
||||||
import GiftFormModal from './components/gift-form-modal.vue';
|
import GiftFormModal from './components/gift-form-modal.vue';
|
||||||
import { GIFT_CONFIG_TAB_OPTIONS } from './constants';
|
import { GIFT_CONFIG_TAB_OPTIONS } from './constants';
|
||||||
|
|
||||||
@ -87,6 +88,7 @@ const settingQuery = reactive({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const giftFormOpen = ref(false);
|
const giftFormOpen = ref(false);
|
||||||
|
const giftBatchOpen = ref(false);
|
||||||
const activeGiftRow = ref<null | Record<string, any>>(null);
|
const activeGiftRow = ref<null | Record<string, any>>(null);
|
||||||
const addRegionOpen = ref(false);
|
const addRegionOpen = ref(false);
|
||||||
const addRegionSaving = ref(false);
|
const addRegionSaving = ref(false);
|
||||||
@ -130,7 +132,6 @@ const cpForm = reactive({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const settingColumns = [
|
const settingColumns = [
|
||||||
{ dataIndex: 'id', key: 'id', title: 'ID', width: 120 },
|
|
||||||
{ dataIndex: 'giftCode', key: 'giftCode', title: 'Code', width: 160 },
|
{ dataIndex: 'giftCode', key: 'giftCode', title: 'Code', width: 160 },
|
||||||
{ dataIndex: 'giftPhoto', key: 'giftPhoto', title: '封面', width: 100 },
|
{ dataIndex: 'giftPhoto', key: 'giftPhoto', title: '封面', width: 100 },
|
||||||
{ dataIndex: 'giftName', key: 'giftName', title: '名称', width: 160 },
|
{ dataIndex: 'giftName', key: 'giftName', title: '名称', width: 160 },
|
||||||
@ -139,10 +140,10 @@ const settingColumns = [
|
|||||||
{ dataIndex: 'regionNameStr', key: 'regionNameStr', title: '区域', width: 180 },
|
{ dataIndex: 'regionNameStr', key: 'regionNameStr', title: '区域', width: 180 },
|
||||||
{ dataIndex: 'typeName', key: 'typeName', title: '类型', width: 120 },
|
{ dataIndex: 'typeName', key: 'typeName', title: '类型', width: 120 },
|
||||||
{ dataIndex: 'sort', key: 'sort', title: '排序权重', width: 100 },
|
{ dataIndex: 'sort', key: 'sort', title: '排序权重', width: 100 },
|
||||||
{ dataIndex: 'del', key: 'del', title: '上/下架', width: 100 },
|
{ align: 'center' as const, className: 'switch-column', dataIndex: 'del', fixed: 'right' as const, key: 'del', title: '上/下架', width: 84 },
|
||||||
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
|
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
|
||||||
{ dataIndex: 'userBaseInfo', key: 'userBaseInfo', title: '专属礼物归属人', width: 180 },
|
{ dataIndex: 'userBaseInfo', key: 'userBaseInfo', title: '专属礼物归属人', width: 180 },
|
||||||
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 180 },
|
{ align: 'center' as const, className: 'action-column', dataIndex: 'actions', fixed: 'right' as const, key: 'actions', title: '操作', width: 84 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const regionOptions = computed(() =>
|
const regionOptions = computed(() =>
|
||||||
@ -249,20 +250,34 @@ function handleWeekStarPageChange(page: number, pageSize: number) {
|
|||||||
void loadWeekStar();
|
void loadWeekStar();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCopy(url?: string) {
|
|
||||||
if (!url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await copyText(getAccessImgUrl(url));
|
|
||||||
message.success('复制成功');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSwitch(record: Record<string, any>) {
|
async function handleSwitch(record: Record<string, any>) {
|
||||||
await switchDelStatus(record.id, record.del);
|
await switchDelStatus(record.id, record.del);
|
||||||
message.success('操作成功');
|
message.success('操作成功');
|
||||||
await loadSetting(false);
|
await loadSetting(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stringifyCellValue(value: any) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumnValue(record: Record<string, any>, column: Record<string, any>) {
|
||||||
|
const dataIndex = column?.dataIndex;
|
||||||
|
if (Array.isArray(dataIndex)) {
|
||||||
|
let value = record;
|
||||||
|
for (const key of dataIndex) {
|
||||||
|
value = value?.[String(key)];
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (dataIndex === undefined || dataIndex === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return record?.[String(dataIndex)];
|
||||||
|
}
|
||||||
|
|
||||||
function openGiftCreate() {
|
function openGiftCreate() {
|
||||||
activeGiftRow.value = null;
|
activeGiftRow.value = null;
|
||||||
giftFormOpen.value = true;
|
giftFormOpen.value = true;
|
||||||
@ -434,26 +449,30 @@ async function handleCpSave() {
|
|||||||
<Button type="primary" @click="openGiftCreate">
|
<Button type="primary" @click="openGiftCreate">
|
||||||
新增礼物
|
新增礼物
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="primary" @click="giftBatchOpen = true">
|
||||||
|
批量导入
|
||||||
|
</Button>
|
||||||
<Button danger @click="openAddRegionModal">
|
<Button danger @click="openAddRegionModal">
|
||||||
添加全部礼物区域
|
添加全部礼物区域
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Table
|
<AdminConfigTable
|
||||||
:columns="settingColumns"
|
:columns="settingColumns"
|
||||||
:data-source="settingList"
|
:data-source="settingList"
|
||||||
:loading="settingLoading"
|
:loading="settingLoading"
|
||||||
:pagination="false"
|
:current="settingQuery.cursor"
|
||||||
|
:page-size="settingQuery.limit"
|
||||||
|
plain
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:scroll="{ x: 1680 }"
|
:scroll-x="1560"
|
||||||
|
:total="settingTotal"
|
||||||
|
@page-change="handleSettingPageChange"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'giftPhoto'">
|
<template v-if="column.key === 'giftPhoto'">
|
||||||
<div class="week-star-gift">
|
<div class="week-star-gift">
|
||||||
<Image :src="getAccessImgUrl(record.giftPhoto)" class="gift-image" />
|
<Image :src="getAccessImgUrl(record.giftPhoto)" class="gift-image" />
|
||||||
<Button size="small" type="link" @click="handleCopy(record.giftPhoto)">
|
|
||||||
复制
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'del'">
|
<template v-else-if="column.key === 'del'">
|
||||||
@ -473,19 +492,11 @@ async function handleCpSave() {
|
|||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ stringifyCellValue(getColumnValue(record, column)) }}
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</AdminConfigTable>
|
||||||
|
|
||||||
<div class="pager">
|
|
||||||
<Pagination
|
|
||||||
:current="settingQuery.cursor"
|
|
||||||
:page-size="settingQuery.limit"
|
|
||||||
:total="settingTotal"
|
|
||||||
show-size-changer
|
|
||||||
@change="handleSettingPageChange"
|
|
||||||
@show-size-change="handleSettingPageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="activeTab === 'weekStar'">
|
<div v-else-if="activeTab === 'weekStar'">
|
||||||
@ -588,6 +599,14 @@ async function handleCpSave() {
|
|||||||
@success="loadSetting(true)"
|
@success="loadSetting(true)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<GiftBatchDrawer
|
||||||
|
:initial-sys-origin="settingQuery.sysOrigin"
|
||||||
|
:open="giftBatchOpen"
|
||||||
|
:sys-origin-options="sysOriginOptions"
|
||||||
|
@close="giftBatchOpen = false"
|
||||||
|
@success="loadSetting(true)"
|
||||||
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
:confirm-loading="addRegionSaving"
|
:confirm-loading="addRegionSaving"
|
||||||
:open="addRegionOpen"
|
:open="addRegionOpen"
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import {
|
|||||||
simpleUploadFile,
|
simpleUploadFile,
|
||||||
} from '#/api/legacy/oss';
|
} from '#/api/legacy/oss';
|
||||||
import { addPropsSource } from '#/api/legacy/props';
|
import { addPropsSource } from '#/api/legacy/props';
|
||||||
|
import { normalizeResourceCode } from '#/views/_shared/resource-code';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BADGE_DISPLAY_TYPE_OPTIONS,
|
BADGE_DISPLAY_TYPE_OPTIONS,
|
||||||
@ -127,71 +128,6 @@ const STRUCTURED_ASSET_KIND_MAP: Record<string, StructuredAssetKind> = {
|
|||||||
animation: 'source',
|
animation: 'source',
|
||||||
cover: 'cover',
|
cover: 'cover',
|
||||||
};
|
};
|
||||||
const RESOURCE_NAME_TRANSLATION_ENTRIES = ([
|
|
||||||
['头像框', 'avatar frame'],
|
|
||||||
['座驾', 'ride'],
|
|
||||||
['坐骑', 'mount'],
|
|
||||||
['勋章', 'badge'],
|
|
||||||
['徽章', 'badge'],
|
|
||||||
['管理员', 'admin'],
|
|
||||||
['圣诞', 'christmas'],
|
|
||||||
['新年', 'new year'],
|
|
||||||
['情人节', 'valentine'],
|
|
||||||
['万圣节', 'halloween'],
|
|
||||||
['锦鲤', 'koi'],
|
|
||||||
['熊猫', 'panda'],
|
|
||||||
['独角兽', 'unicorn'],
|
|
||||||
['天使', 'angel'],
|
|
||||||
['恶魔', 'devil'],
|
|
||||||
['皇冠', 'crown'],
|
|
||||||
['国王', 'king'],
|
|
||||||
['女王', 'queen'],
|
|
||||||
['玫瑰', 'rose'],
|
|
||||||
['星光', 'starlight'],
|
|
||||||
['星星', 'star'],
|
|
||||||
['月亮', 'moon'],
|
|
||||||
['太阳', 'sun'],
|
|
||||||
['彩虹', 'rainbow'],
|
|
||||||
['火焰', 'flame'],
|
|
||||||
['冰雪', 'snow'],
|
|
||||||
['蓝色', 'blue'],
|
|
||||||
['红色', 'red'],
|
|
||||||
['紫色', 'purple'],
|
|
||||||
['粉色', 'pink'],
|
|
||||||
['金色', 'gold'],
|
|
||||||
['银色', 'silver'],
|
|
||||||
['黑色', 'black'],
|
|
||||||
['白色', 'white'],
|
|
||||||
['绿色', 'green'],
|
|
||||||
['黄色', 'yellow'],
|
|
||||||
['钻石', 'diamond'],
|
|
||||||
['水晶', 'crystal'],
|
|
||||||
['幸运', 'lucky'],
|
|
||||||
['浪漫', 'romantic'],
|
|
||||||
['荣耀', 'glory'],
|
|
||||||
['守护', 'guard'],
|
|
||||||
['贵族', 'noble'],
|
|
||||||
['梦幻', 'dream'],
|
|
||||||
['闪耀', 'shining'],
|
|
||||||
['限定', 'limited'],
|
|
||||||
['豪华', 'deluxe'],
|
|
||||||
['高级', 'premium'],
|
|
||||||
['经典', 'classic'],
|
|
||||||
['热气球', 'hot air balloon'],
|
|
||||||
['跑车', 'sports car'],
|
|
||||||
['飞船', 'spaceship'],
|
|
||||||
['游艇', 'yacht'],
|
|
||||||
['摩托', 'motorcycle'],
|
|
||||||
['龙', 'dragon'],
|
|
||||||
['凤', 'phoenix'],
|
|
||||||
['虎', 'tiger'],
|
|
||||||
['兔', 'rabbit'],
|
|
||||||
['猫', 'cat'],
|
|
||||||
['狗', 'dog'],
|
|
||||||
['鱼', 'fish'],
|
|
||||||
] as Array<[string, string]>).toSorted(
|
|
||||||
(current, next) => next[0].length - current[0].length,
|
|
||||||
);
|
|
||||||
|
|
||||||
function isAmountRequiredByType(type: string) {
|
function isAmountRequiredByType(type: string) {
|
||||||
return !['CUSTOMIZE', 'FRAGMENTS'].includes(type);
|
return !['CUSTOMIZE', 'FRAGMENTS'].includes(type);
|
||||||
@ -348,43 +284,8 @@ function normalizeAmount(value: unknown) {
|
|||||||
return Number.isFinite(amount) && amount >= 0 ? amount : 0;
|
return Number.isFinite(amount) && amount >= 0 ? amount : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeBaseName(value: string) {
|
|
||||||
const name = value
|
|
||||||
.replace(/\.[^.]+$/, '')
|
|
||||||
.replace(/([_-]?(cover|covers|source|resource|resources|ios|android|expand))$/i, '')
|
|
||||||
.replace(/([_-]?(封面|资源))$/i, '')
|
|
||||||
.trim();
|
|
||||||
return name || value.replace(/\.[^.]+$/, '') || 'resource';
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeCode(value: string) {
|
function normalizeCode(value: string) {
|
||||||
const baseName = normalizeBaseName(value);
|
return normalizeResourceCode(value);
|
||||||
const translated = translateResourceName(baseName);
|
|
||||||
return slugifyResourceCode(translated) || slugifyResourceCode(baseName) || 'resource';
|
|
||||||
}
|
|
||||||
|
|
||||||
function translateResourceName(value: string) {
|
|
||||||
let result = value.trim();
|
|
||||||
RESOURCE_NAME_TRANSLATION_ENTRIES.forEach(([source, target]) => {
|
|
||||||
result = result.replaceAll(source, ` ${target} `);
|
|
||||||
});
|
|
||||||
return [...result]
|
|
||||||
.map((char) => {
|
|
||||||
if (/[\u4E00-\u9FFF]/.test(char)) {
|
|
||||||
return ` u${char.codePointAt(0)?.toString(16) || ''} `;
|
|
||||||
}
|
|
||||||
return char;
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function slugifyResourceCode(value: string) {
|
|
||||||
return value
|
|
||||||
.normalize('NFKC')
|
|
||||||
.replaceAll(/[^\d A-Za-z]+/g, ' ')
|
|
||||||
.trim()
|
|
||||||
.replaceAll(/\s+/g, '_')
|
|
||||||
.toLowerCase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPathFileName(path: string) {
|
function getPathFileName(path: string) {
|
||||||
|
|||||||
@ -2,8 +2,6 @@
|
|||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
nextTick,
|
nextTick,
|
||||||
onBeforeUnmount,
|
|
||||||
onMounted,
|
|
||||||
reactive,
|
reactive,
|
||||||
ref,
|
ref,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
@ -19,16 +17,13 @@ import { useAccessStore } from '@vben/stores';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
Empty,
|
Empty,
|
||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
message,
|
message,
|
||||||
Pagination,
|
|
||||||
Popover,
|
Popover,
|
||||||
Select,
|
Select,
|
||||||
Switch,
|
Switch,
|
||||||
Table,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'antdv-next';
|
} from 'antdv-next';
|
||||||
|
|
||||||
@ -39,6 +34,7 @@ import {
|
|||||||
pagePropsSource,
|
pagePropsSource,
|
||||||
updatePropsSource,
|
updatePropsSource,
|
||||||
} from '#/api/legacy/props';
|
} from '#/api/legacy/props';
|
||||||
|
import AdminConfigTable from '#/views/_shared/admin-config-table.vue';
|
||||||
import { getAllowedSysOrigins } from '#/views/system/shared';
|
import { getAllowedSysOrigins } from '#/views/system/shared';
|
||||||
|
|
||||||
import ResourceBatchDrawer from './components/resource-batch-drawer.vue';
|
import ResourceBatchDrawer from './components/resource-batch-drawer.vue';
|
||||||
@ -80,20 +76,15 @@ const amountSavingId = ref<null | string>(null);
|
|||||||
const amountDraft = ref<null | number>(null);
|
const amountDraft = ref<null | number>(null);
|
||||||
const filterOpen = reactive<Record<string, boolean>>({});
|
const filterOpen = reactive<Record<string, boolean>>({});
|
||||||
const pageActionOpen = ref(false);
|
const pageActionOpen = ref(false);
|
||||||
const tableAreaRef = ref<HTMLDivElement | null>(null);
|
|
||||||
const pagerRef = ref<HTMLDivElement | null>(null);
|
|
||||||
const tableScrollY = ref(320);
|
|
||||||
const amountFormatter = new Intl.NumberFormat('zh-CN', {
|
const amountFormatter = new Intl.NumberFormat('zh-CN', {
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
});
|
});
|
||||||
const tableScroll = computed(() => ({ x: 1000, y: tableScrollY.value }));
|
|
||||||
const adminFreeOptions = [
|
const adminFreeOptions = [
|
||||||
{ label: '是', value: true as any },
|
{ label: '是', value: true as any },
|
||||||
{ label: '否', value: false as any },
|
{ label: '否', value: false as any },
|
||||||
];
|
];
|
||||||
const DEFAULT_STORE_CURRENCY_TYPES = 'GOLD';
|
const DEFAULT_STORE_CURRENCY_TYPES = 'GOLD';
|
||||||
const DEFAULT_STORE_VALID_DAYS = '7';
|
const DEFAULT_STORE_VALID_DAYS = '7';
|
||||||
let tableResizeObserver: null | ResizeObserver = null;
|
|
||||||
|
|
||||||
const query = reactive({
|
const query = reactive({
|
||||||
cursor: 1,
|
cursor: 1,
|
||||||
@ -139,6 +130,21 @@ function stringifyCellValue(value: any) {
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getColumnValue(record: Record<string, any>, column: Record<string, any>) {
|
||||||
|
const dataIndex = column?.dataIndex;
|
||||||
|
if (Array.isArray(dataIndex)) {
|
||||||
|
let value = record;
|
||||||
|
for (const key of dataIndex) {
|
||||||
|
value = value?.[String(key)];
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (dataIndex === undefined || dataIndex === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return record?.[String(dataIndex)];
|
||||||
|
}
|
||||||
|
|
||||||
function isFilterableColumn(key: unknown) {
|
function isFilterableColumn(key: unknown) {
|
||||||
return filterableColumnKeys.has(String(key));
|
return filterableColumnKeys.has(String(key));
|
||||||
}
|
}
|
||||||
@ -264,7 +270,6 @@ async function loadData(reset = false) {
|
|||||||
total.value = result.total || 0;
|
total.value = result.total || 0;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
void refreshTableScroll();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,25 +294,6 @@ async function attachStoreCommodities(records: Array<Record<string, any>>) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTableScrollY() {
|
|
||||||
const area = tableAreaRef.value;
|
|
||||||
if (!area) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const headerHeight =
|
|
||||||
area.querySelector('.ant-table-thead')?.getBoundingClientRect().height || 48;
|
|
||||||
const scrollbarReserve = 8;
|
|
||||||
tableScrollY.value = Math.max(
|
|
||||||
160,
|
|
||||||
Math.floor(area.clientHeight - headerHeight - scrollbarReserve),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshTableScroll() {
|
|
||||||
await nextTick();
|
|
||||||
updateTableScrollY();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePageChange(page: number, pageSize: number) {
|
function handlePageChange(page: number, pageSize: number) {
|
||||||
query.cursor = page;
|
query.cursor = page;
|
||||||
query.limit = pageSize;
|
query.limit = pageSize;
|
||||||
@ -499,28 +485,6 @@ function openEdit(record: Record<string, any>) {
|
|||||||
formOpen.value = true;
|
formOpen.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await nextTick();
|
|
||||||
void refreshTableScroll();
|
|
||||||
if (typeof ResizeObserver !== 'undefined') {
|
|
||||||
tableResizeObserver = new ResizeObserver(() => {
|
|
||||||
void refreshTableScroll();
|
|
||||||
});
|
|
||||||
if (tableAreaRef.value) {
|
|
||||||
tableResizeObserver.observe(tableAreaRef.value);
|
|
||||||
}
|
|
||||||
if (pagerRef.value) {
|
|
||||||
tableResizeObserver.observe(pagerRef.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.addEventListener('resize', updateTableScrollY);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
tableResizeObserver?.disconnect();
|
|
||||||
window.removeEventListener('resize', updateTableScrollY);
|
|
||||||
});
|
|
||||||
|
|
||||||
loadData(true);
|
loadData(true);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -539,17 +503,18 @@ loadData(true);
|
|||||||
@success="loadData()"
|
@success="loadData()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card v-if="canQuery" class="resource-card">
|
<AdminConfigTable
|
||||||
<div ref="tableAreaRef" class="resource-table-area">
|
v-if="canQuery"
|
||||||
<Table
|
auto-height
|
||||||
class="resource-table"
|
:columns="columns"
|
||||||
:columns="columns"
|
:current="query.cursor"
|
||||||
:data-source="list"
|
:data-source="list"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="false"
|
:page-size="query.limit"
|
||||||
row-key="id"
|
:scroll-x="1000"
|
||||||
:scroll="tableScroll"
|
:total="total"
|
||||||
>
|
@page-change="handlePageChange"
|
||||||
|
>
|
||||||
<template #headerCell="{ column }">
|
<template #headerCell="{ column }">
|
||||||
<template v-if="column.key === 'actions'">
|
<template v-if="column.key === 'actions'">
|
||||||
<div class="column-title column-title--actions">
|
<div class="column-title column-title--actions">
|
||||||
@ -786,24 +751,14 @@ loadData(true);
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ stringifyCellValue(getColumnValue(record, column)) }}
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</template>
|
||||||
</div>
|
</AdminConfigTable>
|
||||||
|
<div v-else class="resource-permission-empty">
|
||||||
<div ref="pagerRef" class="pager">
|
|
||||||
<Pagination
|
|
||||||
:current="query.cursor"
|
|
||||||
:page-size="query.limit"
|
|
||||||
:total="total"
|
|
||||||
show-size-changer
|
|
||||||
@change="handlePageChange"
|
|
||||||
@show-size-change="handlePageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card v-else class="resource-card">
|
|
||||||
<Empty description="暂无查看权限" />
|
<Empty description="暂无查看权限" />
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
<ResourceBatchDrawer
|
<ResourceBatchDrawer
|
||||||
:initial-sys-origin="query.sysOrigin"
|
:initial-sys-origin="query.sysOrigin"
|
||||||
@ -828,14 +783,6 @@ loadData(true);
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pager {
|
|
||||||
border-top: 1px solid rgb(237 240 244);
|
|
||||||
display: flex;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: 10px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-title {
|
.column-title {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -935,110 +882,15 @@ loadData(true);
|
|||||||
color: rgb(22 119 255);
|
color: rgb(22 119 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
.resource-card {
|
.resource-permission-empty {
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
border: 1px solid rgb(229 231 235);
|
border: 1px solid rgb(229 231 235);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: none;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
flex-direction: column;
|
justify-content: center;
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-card :deep(.ant-card-body) {
|
|
||||||
display: flex;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table-area {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table :deep(.ant-table) {
|
|
||||||
color: rgb(52 64 84);
|
|
||||||
font-size: var(--density-table-body-font-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table :deep(.ant-spin-nested-loading),
|
|
||||||
.resource-table :deep(.ant-spin-container) {
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table :deep(.ant-spin-container) {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table :deep(.ant-table) {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table :deep(.ant-table-container) {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table :deep(.ant-table-thead > tr > th) {
|
|
||||||
background: #fafafa;
|
|
||||||
border-bottom-color: rgb(237 240 244);
|
|
||||||
color: rgb(71 84 103);
|
|
||||||
font-size: var(--density-table-header-font-size);
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.35;
|
|
||||||
height: var(--density-table-header-height);
|
|
||||||
padding: 8px var(--density-table-cell-padding-x);
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table :deep(.ant-table-thead > tr > th::before) {
|
|
||||||
background-color: rgb(237 240 244) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table :deep(.ant-table-tbody > tr > td) {
|
|
||||||
border-bottom-color: rgb(237 240 244);
|
|
||||||
height: var(--density-table-row-height);
|
|
||||||
line-height: 1.35;
|
|
||||||
padding: 3px var(--density-table-cell-padding-x);
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table :deep(.ant-table-body) {
|
|
||||||
overflow-y: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table :deep(.switch-column) {
|
|
||||||
padding-left: var(--density-table-fixed-cell-padding-x) !important;
|
|
||||||
padding-right: var(--density-table-fixed-cell-padding-x) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table :deep(.action-column) {
|
|
||||||
padding-left: var(--density-table-fixed-cell-padding-x) !important;
|
|
||||||
padding-right: var(--density-table-fixed-cell-padding-x) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table :deep(.ant-table-tbody > tr:hover > td) {
|
|
||||||
background: rgb(250 252 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table :deep(.ant-table-cell-fix-left),
|
|
||||||
.resource-table :deep(.ant-table-cell-fix-right) {
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-table :deep(.ant-table-tbody > tr:hover > .ant-table-cell-fix-left),
|
|
||||||
.resource-table :deep(.ant-table-tbody > tr:hover > .ant-table-cell-fix-right) {
|
|
||||||
background: rgb(250 252 255);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-switch {
|
.compact-switch {
|
||||||
|
|||||||
@ -75,6 +75,7 @@ const roomDetailsOpen = ref(false);
|
|||||||
const activeRoomId = ref<number | string>('');
|
const activeRoomId = ref<number | string>('');
|
||||||
|
|
||||||
const form = reactive<Record<string, any>>({
|
const form = reactive<Record<string, any>>({
|
||||||
|
autoRewardEnabled: true,
|
||||||
configured: false,
|
configured: false,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
id: 0,
|
id: 0,
|
||||||
@ -140,6 +141,7 @@ const enabledLevelCount = computed(() =>
|
|||||||
|
|
||||||
function normalizeConfig(result: Record<string, any> | null | undefined) {
|
function normalizeConfig(result: Record<string, any> | null | undefined) {
|
||||||
const value = result || {};
|
const value = result || {};
|
||||||
|
form.autoRewardEnabled = value.autoRewardEnabled !== false;
|
||||||
form.configured = Boolean(value.configured);
|
form.configured = Boolean(value.configured);
|
||||||
form.enabled = Boolean(value.enabled);
|
form.enabled = Boolean(value.enabled);
|
||||||
form.id = value.id || 0;
|
form.id = value.id || 0;
|
||||||
@ -282,6 +284,7 @@ async function submitForm() {
|
|||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
await saveRoomTurnoverRewardConfig({
|
await saveRoomTurnoverRewardConfig({
|
||||||
|
autoRewardEnabled: Boolean(form.autoRewardEnabled),
|
||||||
enabled: Boolean(form.enabled),
|
enabled: Boolean(form.enabled),
|
||||||
id: form.id || 0,
|
id: form.id || 0,
|
||||||
levelConfigs: (form.levelConfigs || []).map((item: Record<string, any>) => ({
|
levelConfigs: (form.levelConfigs || []).map((item: Record<string, any>) => ({
|
||||||
@ -386,6 +389,9 @@ watch(
|
|||||||
<Tag :color="form.enabled ? 'success' : 'default'">
|
<Tag :color="form.enabled ? 'success' : 'default'">
|
||||||
{{ form.enabled ? '已启用' : '已关闭' }}
|
{{ form.enabled ? '已启用' : '已关闭' }}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
<Tag :color="form.autoRewardEnabled ? 'success' : 'warning'">
|
||||||
|
{{ form.autoRewardEnabled ? '自动发放' : '手动领取' }}
|
||||||
|
</Tag>
|
||||||
<Tag color="processing">按达到的最高档发放一次</Tag>
|
<Tag color="processing">按达到的最高档发放一次</Tag>
|
||||||
<Button :loading="loading" @click="reloadAll">刷新</Button>
|
<Button :loading="loading" @click="reloadAll">刷新</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -394,6 +400,8 @@ watch(
|
|||||||
<Space align="center" wrap>
|
<Space align="center" wrap>
|
||||||
<span class="field-label">启用</span>
|
<span class="field-label">启用</span>
|
||||||
<Switch v-model:checked="form.enabled" />
|
<Switch v-model:checked="form.enabled" />
|
||||||
|
<span class="field-label">自动发放</span>
|
||||||
|
<Switch v-model:checked="form.autoRewardEnabled" />
|
||||||
<span class="field-label">结算时区</span>
|
<span class="field-label">结算时区</span>
|
||||||
<Input v-model:value="form.timezone" style="width: 180px" />
|
<Input v-model:value="form.timezone" style="width: 180px" />
|
||||||
<span class="sub-text">更新时间:{{ form.updateTime || '-' }}</span>
|
<span class="sub-text">更新时间:{{ form.updateTime || '-' }}</span>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, reactive, ref, watch } from 'vue';
|
import { computed, nextTick, 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';
|
||||||
@ -9,16 +9,16 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
|
message,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
Switch,
|
Switch,
|
||||||
Table,
|
Table,
|
||||||
Tabs,
|
|
||||||
TabPane,
|
TabPane,
|
||||||
|
Tabs,
|
||||||
Tag,
|
Tag,
|
||||||
TextArea,
|
TextArea,
|
||||||
message,
|
|
||||||
} from 'antdv-next';
|
} from 'antdv-next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -37,6 +37,30 @@ defineOptions({ name: 'ResidentVoiceRoomRocketPage' });
|
|||||||
|
|
||||||
const RIYADH_TIMEZONE = 'Asia/Riyadh';
|
const RIYADH_TIMEZONE = 'Asia/Riyadh';
|
||||||
const DEFAULT_MAX_LEVEL = 6;
|
const DEFAULT_MAX_LEVEL = 6;
|
||||||
|
const RULE_TEXT_LOCALES = [
|
||||||
|
{ label: 'English', value: 'en' },
|
||||||
|
{ label: '中文', value: 'zh-CN' },
|
||||||
|
];
|
||||||
|
const RULE_TEXT_FONT_SIZE_OPTIONS = [
|
||||||
|
{ label: '12px', value: '12px' },
|
||||||
|
{ label: '14px', value: '14px' },
|
||||||
|
{ label: '16px', value: '16px' },
|
||||||
|
{ label: '18px', value: '18px' },
|
||||||
|
{ label: '20px', value: '20px' },
|
||||||
|
{ label: '24px', value: '24px' },
|
||||||
|
{ label: '28px', value: '28px' },
|
||||||
|
{ label: '32px', value: '32px' },
|
||||||
|
];
|
||||||
|
const RULE_TEXT_COLOR_OPTIONS = [
|
||||||
|
{ label: '默认黑', value: '#111827' },
|
||||||
|
{ label: '灰色', value: '#6b7280' },
|
||||||
|
{ label: '红色', value: '#ef4444' },
|
||||||
|
{ label: '橙色', value: '#f97316' },
|
||||||
|
{ label: '黄色', value: '#eab308' },
|
||||||
|
{ label: '绿色', value: '#22c55e' },
|
||||||
|
{ label: '蓝色', value: '#3b82f6' },
|
||||||
|
{ label: '紫色', value: '#8b5cf6' },
|
||||||
|
];
|
||||||
|
|
||||||
type LevelRow = {
|
type LevelRow = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -54,15 +78,15 @@ type RewardRow = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
expireDays?: null | number;
|
expireDays?: null | number;
|
||||||
id?: number | string;
|
id?: number | string;
|
||||||
localKey: string;
|
|
||||||
level: number;
|
level: number;
|
||||||
|
localKey: string;
|
||||||
rewardAmount: number;
|
rewardAmount: number;
|
||||||
|
rewardCopies: number;
|
||||||
rewardCover: string;
|
rewardCover: string;
|
||||||
rewardItemId?: null | number | string;
|
rewardItemId?: null | number | string;
|
||||||
rewardName: string;
|
rewardName: string;
|
||||||
rewardScene: string;
|
rewardScene: string;
|
||||||
rewardType: string;
|
rewardType: string;
|
||||||
rewardCopies: number;
|
|
||||||
sort: number;
|
sort: number;
|
||||||
weight: number;
|
weight: number;
|
||||||
};
|
};
|
||||||
@ -109,19 +133,28 @@ const configForm = reactive({
|
|||||||
maxLevel: DEFAULT_MAX_LEVEL,
|
maxLevel: DEFAULT_MAX_LEVEL,
|
||||||
rankingTopLimit: 100,
|
rankingTopLimit: 100,
|
||||||
rewardRecordKeepDays: 35,
|
rewardRecordKeepDays: 35,
|
||||||
ruleTextText: '{}',
|
ruleTextMap: Object.fromEntries(
|
||||||
|
RULE_TEXT_LOCALES.map((item) => [item.value, '']),
|
||||||
|
) as Record<string, string>,
|
||||||
timezone: RIYADH_TIMEZONE,
|
timezone: RIYADH_TIMEZONE,
|
||||||
updateTime: '',
|
updateTime: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const levelRows = ref<LevelRow[]>([]);
|
const levelRows = ref<LevelRow[]>([]);
|
||||||
const rewardRows = ref<RewardRow[]>([]);
|
const rewardRows = ref<RewardRow[]>([]);
|
||||||
|
const ruleTextLocale = ref(RULE_TEXT_LOCALES[0]?.value || 'zh-CN');
|
||||||
|
const ruleTextExtra = ref<Record<string, unknown>>({});
|
||||||
|
const ruleEditorRef = ref<HTMLElement | null>(null);
|
||||||
|
const ruleTextColor = ref(RULE_TEXT_COLOR_OPTIONS[0]?.value || '#111827');
|
||||||
|
const ruleTextFontSize = ref(RULE_TEXT_FONT_SIZE_OPTIONS[2]?.value || '16px');
|
||||||
|
const ruleTextSavedRange = ref<null | Range>(null);
|
||||||
|
|
||||||
const resourcePickerOpen = ref(false);
|
const resourcePickerOpen = ref(false);
|
||||||
const resourcePickerTitle = ref('选择资源');
|
const resourcePickerTitle = ref('选择资源');
|
||||||
const resourcePickerDefaultType = ref('ALL_PROPS');
|
const resourcePickerDefaultType = ref('ALL_PROPS');
|
||||||
const resourcePickerFixedType = ref(false);
|
const resourcePickerFixedType = ref(false);
|
||||||
const resourcePickerTarget = ref<
|
const resourcePickerTarget = ref<
|
||||||
|
| null
|
||||||
| {
|
| {
|
||||||
field: LevelResourceField;
|
field: LevelResourceField;
|
||||||
kind: 'level';
|
kind: 'level';
|
||||||
@ -131,7 +164,6 @@ const resourcePickerTarget = ref<
|
|||||||
kind: 'reward';
|
kind: 'reward';
|
||||||
row: Record<string, any>;
|
row: Record<string, any>;
|
||||||
}
|
}
|
||||||
| null
|
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
const sceneOptions = [
|
const sceneOptions = [
|
||||||
@ -232,6 +264,199 @@ function stringifyJSON(value: unknown) {
|
|||||||
return JSON.stringify(value, null, 2);
|
return JSON.stringify(value, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trimEditorHTML(value: string) {
|
||||||
|
const html = String(value || '').trim();
|
||||||
|
return html === '<br>' || html === '<div><br></div>' ? '' : html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractRuleTextHTML(value: unknown) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
const record = value as Record<string, unknown>;
|
||||||
|
const html = record.html ?? record.content ?? record.value;
|
||||||
|
return typeof html === 'string' ? html : JSON.stringify(value, null, 2);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRuleTextEditor() {
|
||||||
|
void nextTick(() => {
|
||||||
|
const editor = ruleEditorRef.value;
|
||||||
|
if (!editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const html = configForm.ruleTextMap[ruleTextLocale.value] || '';
|
||||||
|
if (editor.innerHTML !== html) {
|
||||||
|
editor.innerHTML = html;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRuleText(value: unknown) {
|
||||||
|
const nextMap = Object.fromEntries(
|
||||||
|
RULE_TEXT_LOCALES.map((item) => [item.value, '']),
|
||||||
|
) as Record<string, string>;
|
||||||
|
const extra: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
nextMap['zh-CN'] = value;
|
||||||
|
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
const localeSet = new Set(RULE_TEXT_LOCALES.map((item) => item.value));
|
||||||
|
Object.entries(value as Record<string, unknown>).forEach(([key, item]) => {
|
||||||
|
if (localeSet.has(key)) {
|
||||||
|
nextMap[key] = extractRuleTextHTML(item);
|
||||||
|
} else {
|
||||||
|
extra[key] = item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
configForm.ruleTextMap = nextMap;
|
||||||
|
ruleTextExtra.value = extra;
|
||||||
|
renderRuleTextEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeRuleTextHTML(value: string) {
|
||||||
|
const html = trimEditorHTML(value);
|
||||||
|
if (!html || typeof document === 'undefined') {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.innerHTML = html;
|
||||||
|
container
|
||||||
|
.querySelectorAll('script, style, iframe, object, embed, link, meta')
|
||||||
|
.forEach((node) => node.remove());
|
||||||
|
container.querySelectorAll('*').forEach((node) => {
|
||||||
|
[...node.attributes].forEach((attr) => {
|
||||||
|
const name = attr.name.toLowerCase();
|
||||||
|
const attributeValue = attr.value.trim().toLowerCase();
|
||||||
|
if (name.startsWith('on') || attributeValue.startsWith('javascript:')) {
|
||||||
|
node.removeAttribute(attr.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return trimEditorHTML(container.innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncRuleTextFromEditor(locale = ruleTextLocale.value) {
|
||||||
|
const editor = ruleEditorRef.value;
|
||||||
|
if (!editor || !locale) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
configForm.ruleTextMap[locale] = sanitizeRuleTextHTML(editor.innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRuleTextSelection() {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const editor = ruleEditorRef.value;
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!editor || !selection || selection.rangeCount === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
if (editor.contains(range.commonAncestorContainer)) {
|
||||||
|
ruleTextSavedRange.value = range.cloneRange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreRuleTextSelection() {
|
||||||
|
const editor = ruleEditorRef.value;
|
||||||
|
if (!editor || typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editor.focus();
|
||||||
|
const selection = window.getSelection();
|
||||||
|
const range = ruleTextSavedRange.value;
|
||||||
|
if (!selection || !range || !editor.contains(range.commonAncestorContainer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGeneratedFontTags(fontSize?: string) {
|
||||||
|
const editor = ruleEditorRef.value;
|
||||||
|
if (!editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editor.querySelectorAll('font').forEach((node) => {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
const color = node.getAttribute('color');
|
||||||
|
span.innerHTML = node.innerHTML;
|
||||||
|
if (color) {
|
||||||
|
span.style.color = color;
|
||||||
|
}
|
||||||
|
if (fontSize && node.getAttribute('size')) {
|
||||||
|
span.style.fontSize = fontSize;
|
||||||
|
}
|
||||||
|
node.replaceWith(span);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRuleTextCommand(command: string, value?: string) {
|
||||||
|
restoreRuleTextSelection();
|
||||||
|
document.execCommand(command, false, value);
|
||||||
|
normalizeGeneratedFontTags();
|
||||||
|
syncRuleTextFromEditor();
|
||||||
|
saveRuleTextSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearRuleTextFormat() {
|
||||||
|
applyRuleTextCommand('removeFormat');
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRuleTextFontSize(value: string) {
|
||||||
|
ruleTextFontSize.value = value;
|
||||||
|
restoreRuleTextSelection();
|
||||||
|
document.execCommand('fontSize', false, '7');
|
||||||
|
normalizeGeneratedFontTags(value);
|
||||||
|
syncRuleTextFromEditor();
|
||||||
|
saveRuleTextSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRuleTextColor(value: string) {
|
||||||
|
ruleTextColor.value = value;
|
||||||
|
restoreRuleTextSelection();
|
||||||
|
document.execCommand('foreColor', false, value);
|
||||||
|
normalizeGeneratedFontTags();
|
||||||
|
syncRuleTextFromEditor();
|
||||||
|
saveRuleTextSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRuleTextFontSizeChange(value: string) {
|
||||||
|
applyRuleTextFontSize(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRuleTextColorInput(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement | null;
|
||||||
|
if (target?.value) {
|
||||||
|
applyRuleTextColor(target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRuleTextPaste() {
|
||||||
|
window.setTimeout(() => syncRuleTextFromEditor());
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRuleTextPayload() {
|
||||||
|
syncRuleTextFromEditor();
|
||||||
|
const payload: Record<string, unknown> = { ...ruleTextExtra.value };
|
||||||
|
RULE_TEXT_LOCALES.forEach((item) => {
|
||||||
|
const html = sanitizeRuleTextHTML(configForm.ruleTextMap[item.value] || '');
|
||||||
|
if (html) {
|
||||||
|
payload[item.value] = html;
|
||||||
|
} else {
|
||||||
|
delete payload[item.value];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
function parseJSONText(value: string, label: string) {
|
function parseJSONText(value: string, label: string) {
|
||||||
const raw = String(value || '').trim();
|
const raw = String(value || '').trim();
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
@ -263,7 +488,7 @@ function toOptionalString(value: unknown) {
|
|||||||
return text || undefined;
|
return text || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeConfig(value: Record<string, any> | null | undefined) {
|
function normalizeConfig(value: null | Record<string, any> | undefined) {
|
||||||
const next = value || {};
|
const next = value || {};
|
||||||
configForm.broadcastDurationSeconds = toNumber(
|
configForm.broadcastDurationSeconds = toNumber(
|
||||||
next.broadcastDurationSeconds,
|
next.broadcastDurationSeconds,
|
||||||
@ -280,7 +505,7 @@ function normalizeConfig(value: Record<string, any> | null | undefined) {
|
|||||||
configForm.maxLevel = toNumber(next.maxLevel, DEFAULT_MAX_LEVEL);
|
configForm.maxLevel = toNumber(next.maxLevel, DEFAULT_MAX_LEVEL);
|
||||||
configForm.rankingTopLimit = toNumber(next.rankingTopLimit, 100);
|
configForm.rankingTopLimit = toNumber(next.rankingTopLimit, 100);
|
||||||
configForm.rewardRecordKeepDays = toNumber(next.rewardRecordKeepDays, 35);
|
configForm.rewardRecordKeepDays = toNumber(next.rewardRecordKeepDays, 35);
|
||||||
configForm.ruleTextText = stringifyJSON(next.ruleText);
|
normalizeRuleText(next.ruleText);
|
||||||
configForm.timezone = RIYADH_TIMEZONE;
|
configForm.timezone = RIYADH_TIMEZONE;
|
||||||
configForm.updateTime = String(next.updateTime || '');
|
configForm.updateTime = String(next.updateTime || '');
|
||||||
}
|
}
|
||||||
@ -495,10 +720,7 @@ async function saveConfig() {
|
|||||||
if (energyRuleJson === undefined) {
|
if (energyRuleJson === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ruleText = parseJSONText(configForm.ruleTextText, '规则说明');
|
const ruleText = buildRuleTextPayload();
|
||||||
if (ruleText === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
configSaving.value = true;
|
configSaving.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await saveVoiceRoomRocketConfig({
|
const result = await saveVoiceRoomRocketConfig({
|
||||||
@ -554,7 +776,7 @@ function addRewardRow(scene = 'TOP1', rewardType = 'GOLD') {
|
|||||||
rewardAmount: isNone ? 0 : 1,
|
rewardAmount: isNone ? 0 : 1,
|
||||||
rewardCover: '',
|
rewardCover: '',
|
||||||
rewardItemId: null,
|
rewardItemId: null,
|
||||||
rewardName: isNone ? '轮空' : rewardType === 'GOLD' ? '金币' : '',
|
rewardName: isNone ? '轮空' : (rewardType === 'GOLD' ? '金币' : ''),
|
||||||
rewardScene: scene,
|
rewardScene: scene,
|
||||||
rewardType,
|
rewardType,
|
||||||
rewardCopies: 1,
|
rewardCopies: 1,
|
||||||
@ -692,6 +914,11 @@ watch(
|
|||||||
watch(rewardLevel, () => {
|
watch(rewardLevel, () => {
|
||||||
void loadRewards();
|
void loadRewards();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(ruleTextLocale, (_next, previous) => {
|
||||||
|
syncRuleTextFromEditor(previous);
|
||||||
|
renderRuleTextEditor();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -786,14 +1013,98 @@ watch(rewardLevel, () => {
|
|||||||
class="field-control"
|
class="field-control"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field-row field-wide">
|
<div class="field-row field-wide">
|
||||||
<span>规则说明 JSON</span>
|
<span>规则说明富文本</span>
|
||||||
<TextArea
|
<div class="rule-editor-panel">
|
||||||
v-model:value="configForm.ruleTextText"
|
<div class="rule-editor-toolbar" @mousedown="saveRuleTextSelection">
|
||||||
:rows="4"
|
<Space wrap>
|
||||||
class="field-control"
|
<Select
|
||||||
/>
|
v-model:value="ruleTextLocale"
|
||||||
</label>
|
:options="RULE_TEXT_LOCALES"
|
||||||
|
class="rule-editor-locale"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
v-model:value="ruleTextFontSize"
|
||||||
|
:options="RULE_TEXT_FONT_SIZE_OPTIONS"
|
||||||
|
class="rule-editor-size"
|
||||||
|
@change="handleRuleTextFontSizeChange"
|
||||||
|
/>
|
||||||
|
<div class="rule-editor-colors">
|
||||||
|
<button
|
||||||
|
v-for="item in RULE_TEXT_COLOR_OPTIONS"
|
||||||
|
:key="item.value"
|
||||||
|
:aria-label="item.label"
|
||||||
|
:class="{
|
||||||
|
'rule-color-swatch--active': ruleTextColor === item.value,
|
||||||
|
}"
|
||||||
|
:style="{ backgroundColor: item.value }"
|
||||||
|
class="rule-color-swatch"
|
||||||
|
type="button"
|
||||||
|
@mousedown.prevent="applyRuleTextColor(item.value)"
|
||||||
|
></button>
|
||||||
|
<input
|
||||||
|
:value="ruleTextColor"
|
||||||
|
aria-label="自定义字体颜色"
|
||||||
|
class="rule-color-picker"
|
||||||
|
type="color"
|
||||||
|
@input="handleRuleTextColorInput"
|
||||||
|
@mousedown="saveRuleTextSelection"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
<Space wrap>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
@mousedown.prevent="applyRuleTextCommand('bold')"
|
||||||
|
>
|
||||||
|
<strong>B</strong>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
@mousedown.prevent="applyRuleTextCommand('italic')"
|
||||||
|
>
|
||||||
|
<em>I</em>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
@mousedown.prevent="applyRuleTextCommand('underline')"
|
||||||
|
>
|
||||||
|
<u>U</u>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
@mousedown.prevent="
|
||||||
|
applyRuleTextCommand('insertUnorderedList')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
无序列表
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
@mousedown.prevent="
|
||||||
|
applyRuleTextCommand('insertOrderedList')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
有序列表
|
||||||
|
</Button>
|
||||||
|
<Button size="small" @mousedown.prevent="clearRuleTextFormat">
|
||||||
|
清除格式
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref="ruleEditorRef"
|
||||||
|
class="rule-editor"
|
||||||
|
contenteditable="true"
|
||||||
|
data-placeholder="请输入规则说明"
|
||||||
|
@blur="syncRuleTextFromEditor()"
|
||||||
|
@keyup="saveRuleTextSelection"
|
||||||
|
@input="syncRuleTextFromEditor()"
|
||||||
|
@mouseup="saveRuleTextSelection"
|
||||||
|
@paste="handleRuleTextPaste"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@ -1152,6 +1463,76 @@ watch(rewardLevel, () => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rule-editor-panel {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-editor-locale {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-editor-size {
|
||||||
|
width: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-editor-colors {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-color-swatch {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-color-swatch--active {
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 0 0 2px rgb(22 119 255 / 16%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-color-picker {
|
||||||
|
width: 28px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-editor {
|
||||||
|
min-height: 180px;
|
||||||
|
max-height: 360px;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
color: #111827;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-editor:empty::before {
|
||||||
|
color: #bfbfbf;
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user