Files
wot-uniapp/src/subPackages/admin/pages/snippets/form.vue
李琦 fe5a5b6c0f 1. 小程序端
2. 视频优化
2026-07-30 14:11:51 +08:00

163 lines
5.3 KiB
Vue

<script setup lang="ts">
/**
* 代码片段表单:代码大文本 + 分类 + 关联文章多选。
*/
import { computed, reactive, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { useToast } from '@wot-ui/ui'
import AppNavbar from '@/components/layout/AppNavbar.vue'
import AppPageShell from '@/components/layout/AppPageShell.vue'
import { useAdminGuard } from '../../composables/useAdminGuard'
import { adminCreate, adminGet, adminList, adminUpdate, normalizeAdminList, type Post, type Snippet } from '@/api'
const toast = useToast()
const { guardAdmin } = useAdminGuard()
const mode = ref<'create' | 'edit'>('create')
const id = ref('')
const loading = ref(false)
const submitting = ref(false)
const codeTypes = ref<{ id: number, name: string }[]>([])
const posts = ref<Post[]>([])
const selectedPosts = ref<number[]>([])
const form = reactive({
title: '',
codeTypeId: '' as string | number,
code: '',
description: '',
})
const pageTitle = computed(() => (mode.value === 'create' ? '新建代码片段' : '编辑代码片段'))
async function load() {
if (!guardAdmin()) return
loading.value = true
try {
codeTypes.value = normalizeAdminList(await adminList('code-types')) as { id: number, name: string }[]
const postData = await adminList('posts', { page: 1, pageSize: 100 })
posts.value = normalizeAdminList(postData) as Post[]
if (mode.value === 'edit' && id.value) {
const data = await adminGet('snippets', id.value) as Snippet
form.title = String(data.title || '')
form.codeTypeId = data.codeTypeId || ''
form.code = String(data.code || '')
form.description = String(data.description || '')
selectedPosts.value = Array.isArray(data.postIds) ? data.postIds.map(Number) : []
}
}
catch (e: unknown) {
toast.error(e instanceof Error ? e.message : '加载失败')
}
finally {
loading.value = false
}
}
onLoad((q) => {
mode.value = q?.mode === 'edit' ? 'edit' : 'create'
id.value = String(q?.id || '')
})
onShow(load)
function togglePost(pid: string | number) {
const n = Number(pid)
const i = selectedPosts.value.indexOf(n)
if (i >= 0) selectedPosts.value.splice(i, 1)
else selectedPosts.value.push(n)
}
async function onSubmit() {
if (!form.title.trim()) {
toast.show('请填写标题')
return
}
submitting.value = true
try {
const payload = {
title: form.title,
codeTypeId: form.codeTypeId ? Number(form.codeTypeId) : undefined,
code: form.code,
description: form.description,
postIds: selectedPosts.value,
}
if (mode.value === 'edit' && id.value) await adminUpdate('snippets', id.value, payload)
else await adminCreate('snippets', payload)
toast.success('已保存')
setTimeout(() => uni.navigateBack(), 400)
}
catch (e: unknown) {
toast.error(e instanceof Error ? e.message : '保存失败')
}
finally {
submitting.value = false
}
}
function typeLabel() {
return codeTypes.value.find(c => String(c.id) === String(form.codeTypeId))?.name || '选择分类'
}
</script>
<template>
<AppPageShell admin>
<AppNavbar :title="pageTitle" show-back />
<view class="page-pad">
<view class="admin-card form-card">
<wd-input v-model="form.title" label="标题" clearable />
<wd-picker
:columns="[codeTypes.map(c => ({ label: c.name, value: c.id }))]"
@confirm="({ value }: { value: unknown[] }) => { form.codeTypeId = (value?.[0] as { value?: number })?.value ?? value?.[0] as number }"
>
<view class="picker-row"><text>分类</text><text class="val">{{ typeLabel() }}</text></view>
</wd-picker>
<wd-textarea v-model="form.code" label="代码" :rows="10" custom-class="code-area" />
<wd-textarea v-model="form.description" label="描述" />
<view class="section">
<text class="label text-art-muted">关联文章</text>
<view class="tag-wrap">
<view
v-for="p in posts"
:key="String(p.id)"
class="tag"
:class="{ on: selectedPosts.includes(Number(p.id)) }"
@click="togglePost(p.id)"
>
{{ p.title }}
</view>
</view>
</view>
<wd-button
block
type="primary"
:loading="submitting"
custom-style="margin-top:24rpx;background:#d4b383;border-color:#d4b383;"
@click="onSubmit"
>
保存
</wd-button>
</view>
<wd-loading v-if="loading" />
</view>
</AppPageShell>
</template>
<style scoped lang="scss">
.page-pad { padding: 24rpx 32rpx 80rpx; }
.form-card { padding: 24rpx; }
.picker-row {
display: flex; justify-content: space-between; align-items: center;
padding: 20rpx 0; color: rgb(var(--art-text));
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.val { color: rgba(255, 255, 255, 0.6); }
.section { margin-top: 16rpx; }
.label { font-size: 24rpx; }
.tag-wrap { display: flex; flex-wrap: wrap; gap: 12rpx; margin-top: 12rpx; }
.tag {
padding: 8rpx 16rpx; border-radius: 8rpx; font-size: 22rpx;
background: rgba(255, 255, 255, 0.06); color: rgba(255, 255, 255, 0.65);
max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tag.on { background: rgba(212, 179, 131, 0.25); color: rgb(var(--art-accent)); }
</style>