576 lines
15 KiB
Vue
576 lines
15 KiB
Vue
<script lang="ts" setup>
|
|
import { reactive, ref } from 'vue';
|
|
|
|
import { useAccessStore } from '@vben/stores';
|
|
|
|
import type {
|
|
LegacyEmojiGroupItem,
|
|
LegacyEmojiItem,
|
|
} from '#/api/legacy/app-system';
|
|
import {
|
|
addOrUpdateEmojiConfig,
|
|
listEmojiGroupsBySysOrigin,
|
|
offShelfEmojiById,
|
|
pageEmojiConfigs,
|
|
} from '#/api/legacy/app-system';
|
|
import {
|
|
OSS_FILE_BUCKETS,
|
|
getAccessImgUrl,
|
|
simpleUploadFile,
|
|
} from '#/api/legacy/oss';
|
|
import SysOriginTag from '#/components/sys-origin-tag.vue';
|
|
import InlineFilterField from '#/views/_shared/inline-filter-field.vue';
|
|
import InlineFilterToolbar from '#/views/_shared/inline-filter-toolbar.vue';
|
|
import {
|
|
EMOJI_SOURCE_TYPE_OPTIONS,
|
|
EMOJI_TYPE_OPTIONS,
|
|
PRODUCT_SHOWCASE_OPTIONS,
|
|
formatDate,
|
|
getAllowedSysOrigins,
|
|
} from '#/views/system/shared';
|
|
|
|
import {
|
|
Button,
|
|
Input,
|
|
Modal,
|
|
Pagination,
|
|
Select,
|
|
SelectOption,
|
|
Switch,
|
|
Table,
|
|
message,
|
|
} from 'antdv-next';
|
|
|
|
const accessStore = useAccessStore();
|
|
|
|
const loading = ref(false);
|
|
const groupLoading = ref(false);
|
|
const submitLoading = ref(false);
|
|
const coverUploading = ref(false);
|
|
const sourceUploading = ref(false);
|
|
const total = ref(0);
|
|
const list = ref<LegacyEmojiItem[]>([]);
|
|
const groupList = ref<LegacyEmojiGroupItem[]>([]);
|
|
const formOpen = ref(false);
|
|
const coverInputRef = ref<HTMLInputElement>();
|
|
const sourceInputRef = ref<HTMLInputElement>();
|
|
|
|
const sysOriginOptions = getAllowedSysOrigins(accessStore.accessCodes || []);
|
|
|
|
const query = reactive<Record<string, any>>({
|
|
cursor: 1,
|
|
groupId: '',
|
|
limit: 20,
|
|
shelfStatus: undefined,
|
|
sysOrigin: sysOriginOptions[0]?.value ?? 'LIKEI',
|
|
type: undefined,
|
|
});
|
|
|
|
const form = reactive<Record<string, any>>({
|
|
coverUrl: '',
|
|
groupId: '',
|
|
id: '',
|
|
shelfStatus: '',
|
|
sort: '',
|
|
sourceType: '',
|
|
sourceUrl: '',
|
|
sysOrigin: '',
|
|
type: '',
|
|
});
|
|
|
|
const columns = [
|
|
{ dataIndex: 'sysOrigin', key: 'sysOrigin', title: '系统', width: 100 },
|
|
{ dataIndex: 'sourceType', key: 'sourceType', title: '资源类型', width: 120 },
|
|
{ dataIndex: 'coverUrl', key: 'coverUrl', title: '封面', width: 120 },
|
|
{ dataIndex: 'groupName', key: 'groupName', title: '分组', width: 160 },
|
|
{ dataIndex: 'type', key: 'type', title: '表情类型', width: 120 },
|
|
{ dataIndex: 'shelfStatus', key: 'shelfStatus', title: '上/下架', width: 120 },
|
|
{ dataIndex: 'sort', key: 'sort', title: '排序', width: 100 },
|
|
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
|
|
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 100 },
|
|
];
|
|
|
|
function createFormState() {
|
|
return {
|
|
coverUrl: '',
|
|
groupId: '',
|
|
id: '',
|
|
shelfStatus: '',
|
|
sort: '',
|
|
sourceType: '',
|
|
sourceUrl: '',
|
|
sysOrigin: '',
|
|
type: '',
|
|
};
|
|
}
|
|
|
|
function resetForm() {
|
|
Object.assign(form, createFormState());
|
|
}
|
|
|
|
async function loadGroupList(reset = false) {
|
|
if (reset) {
|
|
query.groupId = '';
|
|
}
|
|
groupLoading.value = true;
|
|
try {
|
|
groupList.value = await listEmojiGroupsBySysOrigin(query.sysOrigin);
|
|
} finally {
|
|
groupLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function loadData(reset = false) {
|
|
if (reset) {
|
|
query.cursor = 1;
|
|
}
|
|
loading.value = true;
|
|
try {
|
|
const result = await pageEmojiConfigs({ ...query });
|
|
list.value = result.records || [];
|
|
total.value = result.total || 0;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function handlePageChange(page: number, pageSize: number) {
|
|
query.cursor = page;
|
|
query.limit = pageSize;
|
|
loadData();
|
|
}
|
|
|
|
function handleSysOriginChange() {
|
|
loadGroupList(true);
|
|
loadData(true);
|
|
}
|
|
|
|
function openCreate() {
|
|
resetForm();
|
|
formOpen.value = true;
|
|
}
|
|
|
|
function openEdit(record: LegacyEmojiItem) {
|
|
resetForm();
|
|
form.coverUrl = record.coverUrl ?? '';
|
|
form.groupId = record.groupId ?? '';
|
|
form.id = record.id ?? '';
|
|
form.shelfStatus = record.shelfStatus ?? '';
|
|
form.sort = record.sort ?? '';
|
|
form.sourceType = record.sourceType ?? '';
|
|
form.sourceUrl = record.sourceUrl ?? '';
|
|
form.sysOrigin = record.sysOrigin ?? '';
|
|
form.type = record.type ?? '';
|
|
formOpen.value = true;
|
|
}
|
|
|
|
function openCoverPicker() {
|
|
coverInputRef.value?.click();
|
|
}
|
|
|
|
function openSourcePicker() {
|
|
sourceInputRef.value?.click();
|
|
}
|
|
|
|
async function handleCoverFileChange(event: Event) {
|
|
const target = event.target as HTMLInputElement;
|
|
const file = target.files?.[0];
|
|
target.value = '';
|
|
if (!file) {
|
|
return;
|
|
}
|
|
coverUploading.value = true;
|
|
try {
|
|
const result = await simpleUploadFile(file, OSS_FILE_BUCKETS.svgasource);
|
|
form.coverUrl = getAccessImgUrl(result.name);
|
|
message.success('上传成功');
|
|
} finally {
|
|
coverUploading.value = false;
|
|
}
|
|
}
|
|
|
|
async function handleSourceFileChange(event: Event) {
|
|
const target = event.target as HTMLInputElement;
|
|
const file = target.files?.[0];
|
|
target.value = '';
|
|
if (!file) {
|
|
return;
|
|
}
|
|
sourceUploading.value = true;
|
|
try {
|
|
const result = await simpleUploadFile(file, OSS_FILE_BUCKETS.svgasource);
|
|
form.sourceUrl = getAccessImgUrl(result.name);
|
|
message.success('上传成功');
|
|
} finally {
|
|
sourceUploading.value = false;
|
|
}
|
|
}
|
|
|
|
function validateForm() {
|
|
if (!form.groupId) {
|
|
message.warning('请选择分组');
|
|
return false;
|
|
}
|
|
if (!String(form.coverUrl || '').trim()) {
|
|
message.warning('请上传封面');
|
|
return false;
|
|
}
|
|
if (!form.type) {
|
|
message.warning('请选择类型');
|
|
return false;
|
|
}
|
|
if (form.shelfStatus === '' || form.shelfStatus === null || form.shelfStatus === undefined) {
|
|
message.warning('请选择上架状态');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
submitLoading.value = true;
|
|
try {
|
|
await addOrUpdateEmojiConfig({
|
|
...form,
|
|
sysOrigin: query.sysOrigin,
|
|
});
|
|
message.success('保存成功');
|
|
formOpen.value = false;
|
|
resetForm();
|
|
await loadData(true);
|
|
} finally {
|
|
submitLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function handleShelfStatusChange(record: LegacyEmojiItem, checked: boolean) {
|
|
const previous = record.shelfStatus;
|
|
record.shelfStatus = checked;
|
|
try {
|
|
await offShelfEmojiById(record.id as number | string);
|
|
await loadData(true);
|
|
} catch (error) {
|
|
record.shelfStatus = previous;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function getSourceTypeName(value: number | string) {
|
|
return (
|
|
EMOJI_SOURCE_TYPE_OPTIONS.find((item) => item.value === Number(value))?.name ||
|
|
value ||
|
|
'-'
|
|
);
|
|
}
|
|
|
|
function getTypeName(value?: string) {
|
|
return EMOJI_TYPE_OPTIONS.find((item) => item.value === value)?.name || value || '-';
|
|
}
|
|
|
|
loadGroupList(true);
|
|
loadData(true);
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<InlineFilterToolbar class="toolbar">
|
|
<InlineFilterField label="系统">
|
|
<SysOriginSelect
|
|
v-model:value="query.sysOrigin"
|
|
style="width: 140px"
|
|
@change="handleSysOriginChange"
|
|
|
|
:options="sysOriginOptions"
|
|
></SysOriginSelect>
|
|
</InlineFilterField>
|
|
<InlineFilterField label="分组">
|
|
<Select
|
|
v-model:value="query.groupId"
|
|
:loading="groupLoading"
|
|
allow-clear
|
|
option-label-prop="label"
|
|
placeholder="分组"
|
|
style="width: 160px"
|
|
@change="loadData(true)"
|
|
>
|
|
<SelectOption
|
|
v-for="item in groupList"
|
|
:key="item.id"
|
|
:value="item.id"
|
|
:label="`${item.groupName}`">
|
|
{{ item.groupName }}
|
|
</SelectOption>
|
|
</Select>
|
|
</InlineFilterField>
|
|
<InlineFilterField label="类型">
|
|
<Select
|
|
v-model:value="query.type"
|
|
allow-clear
|
|
option-label-prop="label"
|
|
placeholder="类型"
|
|
style="width: 140px"
|
|
@change="loadData(true)"
|
|
>
|
|
<SelectOption
|
|
v-for="item in EMOJI_TYPE_OPTIONS"
|
|
:key="item.value"
|
|
:value="item.value"
|
|
:label="`${item.name}`">
|
|
{{ item.name }}
|
|
</SelectOption>
|
|
</Select>
|
|
</InlineFilterField>
|
|
<InlineFilterField label="状态">
|
|
<Select
|
|
v-model:value="query.shelfStatus"
|
|
allow-clear
|
|
option-label-prop="label"
|
|
placeholder="状态"
|
|
style="width: 140px"
|
|
@change="loadData(true)"
|
|
>
|
|
<SelectOption
|
|
v-for="item in PRODUCT_SHOWCASE_OPTIONS"
|
|
:key="String(item.value)"
|
|
:value="item.value"
|
|
:label="`${item.name}`">
|
|
{{ item.name }}
|
|
</SelectOption>
|
|
</Select>
|
|
</InlineFilterField>
|
|
<Button :loading="loading" type="primary" @click="loadData(true)">
|
|
搜索
|
|
</Button>
|
|
<Button @click="openCreate">新增</Button>
|
|
</InlineFilterToolbar>
|
|
|
|
<Table
|
|
:columns="columns"
|
|
:data-source="list"
|
|
:loading="loading"
|
|
:pagination="false"
|
|
row-key="id"
|
|
:scroll="{ x: 1380 }"
|
|
>
|
|
<template #bodyCell="{ column, record }">
|
|
<template v-if="column.key === 'sysOrigin'">
|
|
<SysOriginTag :value="record.sysOrigin" />
|
|
</template>
|
|
<template v-else-if="column.key === 'sourceType'">
|
|
{{ getSourceTypeName(record.sourceType as number | string) }}
|
|
</template>
|
|
<template v-else-if="column.key === 'coverUrl'">
|
|
<img
|
|
:src="record.coverUrl || 'https://dummyimage.com/56x56/e2e8f0/64748b&text=IMG'"
|
|
alt=""
|
|
class="cover-image"
|
|
>
|
|
</template>
|
|
<template v-else-if="column.key === 'groupName'">
|
|
{{ record.group?.groupName || '-' }}
|
|
</template>
|
|
<template v-else-if="column.key === 'type'">
|
|
{{ getTypeName(record.type) }}
|
|
</template>
|
|
<template v-else-if="column.key === 'shelfStatus'">
|
|
<Switch
|
|
:checked="Boolean(record.shelfStatus)"
|
|
checked-children="上架"
|
|
un-checked-children="下架"
|
|
@change="(checked: boolean) => handleShelfStatusChange(record, checked)"
|
|
/>
|
|
</template>
|
|
<template v-else-if="column.key === 'createTime'">
|
|
{{ formatDate(record.createTime) }}
|
|
</template>
|
|
<template v-else-if="column.key === 'actions'">
|
|
<Button size="small" type="link" @click="openEdit(record)">
|
|
修改
|
|
</Button>
|
|
</template>
|
|
</template>
|
|
</Table>
|
|
|
|
<div class="pager">
|
|
<Pagination
|
|
:current="query.cursor"
|
|
:page-size="query.limit"
|
|
:total="total"
|
|
show-size-changer
|
|
@change="handlePageChange"
|
|
@showSizeChange="handlePageChange"
|
|
/>
|
|
</div>
|
|
|
|
<Modal
|
|
:confirm-loading="submitLoading"
|
|
:open="formOpen"
|
|
destroy-on-close
|
|
ok-text="保存"
|
|
:title="form.id ? '修改' : '添加'"
|
|
width="700"
|
|
@cancel="formOpen = false"
|
|
@ok="handleSubmit"
|
|
>
|
|
<div class="form-grid">
|
|
<div class="form-item">
|
|
<label class="form-label">分组</label>
|
|
<Select option-label-prop="label"
|
|
v-model:value="form.groupId"
|
|
:loading="groupLoading"
|
|
placeholder="请选择分组"
|
|
>
|
|
<SelectOption
|
|
v-for="item in groupList"
|
|
:key="item.id"
|
|
:value="item.id"
|
|
:label="`${item.groupName}`">
|
|
{{ item.groupName }}
|
|
</SelectOption>
|
|
</Select>
|
|
</div>
|
|
|
|
<div class="form-item">
|
|
<label class="form-label">资源类型</label>
|
|
<Select option-label-prop="label" v-model:value="form.sourceType" placeholder="资源类型">
|
|
<SelectOption
|
|
v-for="item in EMOJI_SOURCE_TYPE_OPTIONS"
|
|
:key="item.value"
|
|
:value="item.value"
|
|
:label="`${item.name}`">
|
|
{{ item.name }}
|
|
</SelectOption>
|
|
</Select>
|
|
</div>
|
|
|
|
<div class="form-item">
|
|
<label class="form-label">封面</label>
|
|
<div class="upload-row">
|
|
<Button :loading="coverUploading" @click="openCoverPicker">上传封面</Button>
|
|
<Button v-if="form.coverUrl" type="link" @click="form.coverUrl = ''">清空</Button>
|
|
</div>
|
|
<img
|
|
v-if="form.coverUrl"
|
|
:src="form.coverUrl"
|
|
alt=""
|
|
class="cover-preview"
|
|
>
|
|
<input
|
|
ref="coverInputRef"
|
|
accept="image/*"
|
|
class="hidden-input"
|
|
type="file"
|
|
@change="handleCoverFileChange"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-item">
|
|
<label class="form-label">svga/gif</label>
|
|
<div class="upload-row">
|
|
<Button :loading="sourceUploading" @click="openSourcePicker">点击上传</Button>
|
|
<Button v-if="form.sourceUrl" type="link" @click="form.sourceUrl = ''">清空</Button>
|
|
</div>
|
|
<a
|
|
v-if="form.sourceUrl"
|
|
:href="form.sourceUrl"
|
|
rel="noreferrer"
|
|
target="_blank"
|
|
class="source-link"
|
|
>
|
|
{{ form.sourceUrl }}
|
|
</a>
|
|
<input
|
|
ref="sourceInputRef"
|
|
accept=".svga,.gif,.pag"
|
|
class="hidden-input"
|
|
type="file"
|
|
@change="handleSourceFileChange"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-item">
|
|
<label class="form-label">类型</label>
|
|
<Select option-label-prop="label" v-model:value="form.type" placeholder="类型">
|
|
<SelectOption
|
|
v-for="item in EMOJI_TYPE_OPTIONS"
|
|
:key="item.value"
|
|
:value="item.value"
|
|
:label="`${item.name}`">
|
|
{{ item.name }}
|
|
</SelectOption>
|
|
</Select>
|
|
</div>
|
|
|
|
<div class="form-item">
|
|
<label class="form-label">上架</label>
|
|
<Select option-label-prop="label" v-model:value="form.shelfStatus" placeholder="上架">
|
|
<SelectOption
|
|
v-for="item in PRODUCT_SHOWCASE_OPTIONS"
|
|
:key="String(item.value)"
|
|
:value="item.value"
|
|
:label="`${item.name}`">
|
|
{{ item.name }}
|
|
</SelectOption>
|
|
</Select>
|
|
</div>
|
|
|
|
<div class="form-item">
|
|
<label class="form-label">顺序</label>
|
|
<Input v-model:value="form.sort" placeholder="数字越小越靠前" />
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.toolbar {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.pager {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.cover-image,
|
|
.cover-preview {
|
|
border-radius: 12px;
|
|
height: 56px;
|
|
object-fit: cover;
|
|
width: 56px;
|
|
}
|
|
|
|
.form-grid {
|
|
display: grid;
|
|
gap: 16px;
|
|
}
|
|
|
|
.form-item {
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
|
|
.form-label {
|
|
color: #334155;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.upload-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.hidden-input {
|
|
display: none;
|
|
}
|
|
|
|
.source-link {
|
|
color: #2563eb;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
</style>
|