xiongzhu преди 2 години
родител
ревизия
18d4f2055c

+ 2 - 2
.env.development

@@ -1,3 +1,3 @@
 VITE_BASE_URL=/
-VITE_API_BASE_URL=http://localhost:3000/api
-VITE_WS_URL=ws://localhost:3000
+VITE_API_BASE_URL=http://localhost:3333/api
+VITE_WS_URL=ws://localhost:3333

+ 1 - 1
index.html

@@ -4,7 +4,7 @@
     <meta charset="UTF-8">
     <link rel="icon" href="/favicon.ico">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
-    <title>AIRPG</title>
+    <title>Admin</title>
   </head>
   <body>
     <div id="app"></div>

+ 4 - 0
package.json

@@ -10,11 +10,15 @@
     "format": "prettier --write src/"
   },
   "dependencies": {
+    "@aws-sdk/client-s3": "^3.458.0",
+    "@aws-sdk/s3-request-presigner": "^3.458.0",
     "@vicons/tabler": "^0.12.0",
     "@vueuse/core": "^10.1.0",
+    "ali-oss": "^6.18.1",
     "axios": "^1.3.6",
     "date-fns": "^2.29.3",
     "element-plus": "^2.3.3",
+    "fast-uri": "^2.3.0",
     "pinia": "^2.0.32",
     "qs": "^6.11.1",
     "resolve-url": "^0.2.1",

+ 1 - 1
src/components/MultiUpload.vue

@@ -46,7 +46,7 @@ const props = defineProps({
     }
 })
 const emit = defineEmits(['update:modelValue'])
-const uploadUrl = resolveUrl(import.meta.env.VITE_API_BASE_URL, '/api/file/upload')
+const uploadUrl = resolveUrl(import.meta.env.VITE_API_BASE_URL, '/api/files/upload')
 const fileList = ref([])
 const loading = ref(false)
 const showPreview = ref(false)

+ 14 - 16
src/components/PagingTable.vue

@@ -29,24 +29,24 @@ const props = defineProps({
         type: String,
         required: true
     },
-    where: {
+    query: {
         type: Object,
         default: () => ({})
     },
     order: {
-        type: Object,
-        default: () => ({ createdAt: 'DESC' })
+        type: String,
+        default: () => 'createdAt,desc'
     }
 })
 const search = computed(() => {
-    const where = { ...(props.where || {}) }
-    Object.keys(where).forEach((key) => {
-        if (where[key] === null) {
-            delete where[key]
+    const query = { ...(props.query || {}) }
+    Object.keys(query).forEach((key) => {
+        if (query[key] === null) {
+            delete query[key]
         }
     })
     return {
-        where: props.where,
+        ...query,
         order: props.order
     }
 })
@@ -70,16 +70,14 @@ const loading = ref(false)
 async function getData() {
     try {
         loading.value = true
-        const res = await http.post(props.url, {
-            page: {
-                page: page.value,
-                limit: pageConfig.value.pageSize
-            },
-            search: search.value
+        const res = await http.get(props.url, {
+            page: page.value,
+            pageSize: pageConfig.value.pageSize,
+            ...search.value
         })
         loading.value = false
-        tableData.value = res.items
-        total.value = res.meta.totalItems
+        tableData.value = res.data
+        total.value = res.meta.total
     } catch (e) {
         loading.value = false
         ElMessage.error(e.message)

+ 42 - 0
src/components/SecureImage.vue

@@ -0,0 +1,42 @@
+<template>
+    <ElImage :src="finalSrc" :preview-src-list="previewList" preview-teleported loading="lazy">
+        <template #error>
+            <ElIcon> <IconPicture /></ElIcon>
+        </template>
+    </ElImage>
+</template>
+<script setup>
+import { computed } from 'vue'
+import { Picture as IconPicture } from '@element-plus/icons-vue'
+import uri from 'fast-uri'
+import { ElIcon } from 'element-plus'
+const prefix = 'https://zm-shorts.oss-cn-hangzhou.aliyuncs.com'
+const props = defineProps({
+    src: {
+        type: [String, Array],
+        required: true
+    },
+    fit: {
+        type: String,
+        default: 'cover'
+    }
+})
+const finalSrc = computed(() => {
+    if (!props.src) {
+        return ''
+    }
+    if (Array.isArray(props.src)) {
+        return uri.resolve(prefix, props.src[0])
+    }
+    return uri.resolve(prefix, props.src)
+})
+const previewList = computed(() => {
+    if (!props.src) {
+        return []
+    }
+    if (Array.isArray(props.src)) {
+        return props.src.map((src) => uri.resolve(prefix, src))
+    }
+    return [uri.resolve(prefix, props.src)]
+})
+</script>

+ 7 - 2
src/components/SingleUpload.vue

@@ -8,6 +8,7 @@
         :before-upload="beforeUpload"
         accept="image/*"
         @on-change="onChange"
+        :headers="headers"
     >
         <template #trigger>
             <div
@@ -34,10 +35,11 @@
     <ElImageViewer v-if="showPreview" :url-list="[imageUrl]" teleported @close="showPreview = false" />
 </template>
 <script setup>
-import { ref, unref, watch } from 'vue'
+import { computed, ref, unref, watch } from 'vue'
 import resolveUrl from 'resolve-url'
 import { Plus, Upload, Trash, Eye } from '@vicons/tabler'
 import { useImageSize } from '@/utils/imageSize'
+import { http } from '@/plugins/http'
 const props = defineProps({
     modelValue: String,
     usePrefix: {
@@ -60,8 +62,11 @@ const props = defineProps({
         default: 1024 * 1024
     }
 })
+const headers = computed(() => ({
+    Authorization: `Bearer ${http.token.value}`
+}))
 const emit = defineEmits(['update:modelValue'])
-const uploadUrl = resolveUrl(import.meta.env.VITE_API_BASE_URL, '/api/file/upload')
+const uploadUrl = resolveUrl(import.meta.env.VITE_API_BASE_URL, '/api/files/upload')
 const loading = ref(false)
 const fileList = ref([])
 const imageUrl = ref(null)

+ 2 - 0
src/main.js

@@ -4,6 +4,7 @@ import { createPinia } from 'pinia'
 import App from './App.vue'
 import router from './router'
 import ElementPlus from 'element-plus'
+import SecureImage from '@/components/SecureImage.vue'
 
 import './styles/main.less'
 import 'element-plus/dist/index.css'
@@ -14,5 +15,6 @@ const app = createApp(App)
 app.use(createPinia())
 app.use(router)
 app.use(ElementPlus)
+app.component('SecureImage', SecureImage)
 
 app.mount('#app')

+ 16 - 0
src/plugins/sts.js

@@ -0,0 +1,16 @@
+import OSS from 'ali-oss'
+const client = new OSS({
+    accessKeyId: 'STS.NSz4JRBvYaYfAom59scqdAxyM',
+    accessKeySecret: 'CoweobBccMPKbyMQMX4gEDz6r8aBDBk9FCo5BkgkePUd',
+    expiration: '2023-11-27T12:07:15Z',
+    stsToken:
+        'CAIS6wF1q6Ft5B2yfSjIr5DPf/Dmr6l41puNQ0nc0Tkmb/5Irr3Srzz2IHhMe3VoCO0atv0+nW1S7f0Zlq1uUJJfHbhJsSbZt8Y5yxioRqacke7XhOV2pf/IMGyXDAGBr622Su7lTdTbV+6wYlTf7EFayqf7cjPQND7Mc+f+6/hdY88QQxOzYBdfGd5SPXECksIBMmbLPvvfWXyDwEioVRIz5VIk1DIuufrun5TMtSCz1gOqlrUnwK3qOYWhYsVWO5Nybsy4xuQedNCaiHYPt0Abqfsu1f0dpG2c5YqHY15K+FCcPuPY4ibB/tb96itTGoABohuUPdjrjETtBIL/MYdfSM8tkyy7eg49KU9v8GSDxFi3KomoKlVXF4d7mLlIQkxBSYL2SKFD106mfy3YNumyoBnmizfMwakVFn6t4cCvTDWnqkX60xWtsVwlO3m5zoJbEyv5sKa00ps+Tn65sS8cybF1k91DXIhQJxAOjrIcX98gAA==',
+    region: 'oss-cn-hangzhou',
+    bucket: 'zm-shorts'
+})
+
+export async function sign(obj) {
+    const url = client.signatureUrl(obj)
+    console.log(url)
+    return url
+}

+ 6 - 30
src/router/index.js

@@ -31,7 +31,7 @@ const router = createRouter({
                     }
                 },
                 {
-                    path: '/user',
+                    path: 'user',
                     name: 'user',
                     component: () => import('../views/UserView.vue'),
                     meta: {
@@ -47,35 +47,11 @@ const router = createRouter({
                     }
                 },
                 {
-                    path: '/game',
-                    name: 'game',
-                    component: () => import('../views/GameView.vue'),
+                    path: 'series',
+                    name: 'series',
+                    component: () => import('../views/SeriesView.vue'),
                     meta: {
-                        title: '游戏列表'
-                    }
-                },
-                {
-                    path: '/play/:roomId/:gameId',
-                    name: 'play',
-                    component: () => import('../views/PlayView.vue'),
-                    meta: {
-                        title: '游戏详情'
-                    }
-                },
-                {
-                    path: '/prompt',
-                    name: 'prompt',
-                    component: () => import('../views/PromptView.vue'),
-                    meta: {
-                        title: '提示词'
-                    }
-                },
-                {
-                    path: '/room',
-                    name: 'room',
-                    component: () => import('../views/RoomView.vue'),
-                    meta: {
-                        title: '直播间'
+                        title: '短剧列表'
                     }
                 }
             ]
@@ -105,7 +81,7 @@ router.beforeEach(async (to, from, next) => {
     const { user, setUser } = useUserStore()
     if (!user && to.name !== 'login') {
         try {
-            const res = await http.get('/admin/users/get')
+            const res = await http.get('/users/admin/my')
             setUser(res)
             next()
         } catch (err) {

+ 0 - 318
src/views/GameView.vue

@@ -1,318 +0,0 @@
-<template>
-    <PagingTable url="/game" :where="where" ref="table">
-        <template #filter>
-            <ElSelect v-model="where.roomId" clearable placeholder="筛选房间">
-                <ElOption v-for="item in rooms" :key="item.id" :label="item.name" :value="item.id" />
-            </ElSelect>
-            <ElButton :icon="Plus" @click="onEdit()">添加</ElButton>
-        </template>
-        <ElTableColumn prop="id" label="#" width="80" />
-        <ElTableColumn prop="name" label="名称" />
-        <ElTableColumn prop="type" label="类型" width="100" />
-        <ElTableColumn prop="roomId" label="房间ID" />
-        <ElTableColumn prop="status" label="状态" width="150" :formatter="statusFormatter" />
-        <ElTableColumn prop="autoReset" label="自动重置" />
-        <ElTableColumn prop="resetNum" label="重置次数" />
-        <ElTableColumn prop="running" label="运行" width="100" align="center">
-            <template #default="{ row }">
-                <ElSwitch
-                    v-if="row.status === 'initialized' || row.status === 'running'"
-                    :model-value="row.status === 'running'"
-                    @update:model-value="onActiveChange($event, row)"
-                />
-            </template>
-        </ElTableColumn>
-        <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
-        <ElTableColumn label="操作" align="center" width="350">
-            <template #default="{ row }">
-                <ElButton @click="onEdit(row)" type="primary">编辑</ElButton>
-                <ElButton v-if="row.status === 'created'" @click="onInit(row)" type="primary">初始化</ElButton>
-                <ElButton v-if="row.status !== 'created'" @click="onPlay(row)" type="primary">详情</ElButton>
-                <ElButton v-if="row.status !== 'created'" @click="reset(row)" type="warning">重置</ElButton>
-                <ElButton @click="onDelete(row)" type="danger">删除</ElButton>
-            </template>
-        </ElTableColumn>
-    </PagingTable>
-    <EditDialog
-        v-model="showEditDialog"
-        :model="model"
-        :rules="rules"
-        :on-submit="submit"
-        @success="table.refresh()"
-        width="800px"
-    >
-        <ElFormItem prop="name" label="名称">
-            <ElInput v-model="model.name" placeholder="请输入名称" />
-        </ElFormItem>
-        <ElFormItem prop="type" label="类型">
-            <ElSelect v-model="model.type" placeholder="请选择类型">
-                <ElOption v-for="item in promptTypes" :key="item" :label="item" :value="item" />
-            </ElSelect>
-        </ElFormItem>
-        <ElFormItem prop="roomId" label="房间">
-            <ElSelect v-model="model.roomId" placeholder="请选择房间">
-                <ElOption v-for="item in rooms" :key="item.id" :label="item.name" :value="item.id" />
-            </ElSelect>
-        </ElFormItem>
-        <ElFormItem prop="autoReset" label="自动重置">
-            <ElRadioGroup v-model="model.autoReset">
-                <ElRadio :label="true">是</ElRadio>
-                <ElRadio :label="false">否</ElRadio>
-            </ElRadioGroup>
-        </ElFormItem>
-        <ElFormItem prop="background" label="故事背景">
-            <ElInput v-model="model.background" type="textarea" placeholder="请输入故事背景" />
-        </ElFormItem>
-        <ElFormItem prop="charactors" label="初始角色">
-            <div class="charactor-form" v-for="(item, i) in model.charactors" :key="i">
-                <ElForm :model="item" label-position="top" inline :rules="charactorRules" ref="charactorForms">
-                    <ElFormItem prop="avatar" label="头像">
-                        <SingleUpload v-model="item.avatar" />
-                    </ElFormItem>
-                    <ElFormItem prop="name" label="角色名称">
-                        <ElInput v-model="item.name" />
-                    </ElFormItem>
-                    <ElFormItem prop="gender" label="性别">
-                        <ElInput v-model="item.gender" />
-                    </ElFormItem>
-                    <ElFormItem prop="age" label="年龄">
-                        <ElInput v-model="item.age" />
-                    </ElFormItem>
-                    <ElFormItem prop="occupation" label="职业">
-                        <ElInput v-model="item.occupation" />
-                    </ElFormItem>
-                    <ElFormItem prop="personality" label="性格">
-                        <ElInput v-model="item.personality" />
-                    </ElFormItem>
-                    <div class="w-full"></div>
-                    <ElFormItem class="w-full" prop="background" label="背景">
-                        <ElInput v-model="item.background" type="textarea" />
-                    </ElFormItem>
-                </ElForm>
-                <ElButton type="danger" size="small" @click="removeCharactor(i)">删除</ElButton>
-            </div>
-            <ElButton @click="genCharactor" :loading="generating">生成</ElButton>
-            <ElButton @click="onAddCharactor">添加</ElButton>
-        </ElFormItem>
-    </EditDialog>
-    <ElDialog title="初始化" v-model="showInitDialog">
-        <ElForm :model="initModel" :rules="initRules" ref="initForm" label-position="right" label-width="80">
-            <ElFormItem label="日期" prop="date">
-                <ElDatePicker v-model="initModel.date" />
-            </ElFormItem>
-            <ElFormItem label="时间" prop="time">
-                <ElSelect v-model="initModel.time">
-                    <ElOption label="上午" value="morning" />
-                    <ElOption label="下午" value="afternoon" />
-                    <ElOption label="晚上" value="evening" />
-                </ElSelect>
-            </ElFormItem>
-            <ElFormItem label="初始剧情" prop="plot">
-                <ElInput v-model="initModel.plot" type="textarea" placeholder="不填则自动生成" :rows="3" />
-            </ElFormItem>
-        </ElForm>
-        <template #footer>
-            <ElButton @click="showInitDialog = false">取消</ElButton>
-            <ElButton type="primary" @click="init">确定</ElButton>
-        </template>
-    </ElDialog>
-</template>
-<script setup>
-import { onMounted, ref } from 'vue'
-import PagingTable from '@/components/PagingTable.vue'
-import { useEnumFormatter, useTimeFormatter } from '@/utils/formatter'
-import { Plus } from '@vicons/tabler'
-import EditDialog from '@/components/EditDialog.vue'
-import { setupEditDialog } from '@/utils/editDialog'
-import { http } from '@/plugins/http'
-import { ElMessage, ElMessageBox } from 'element-plus'
-import { useRouter } from 'vue-router'
-import { ElLoading } from 'element-plus'
-import { GameStatus } from '@/enums'
-
-const router = useRouter()
-const where = ref({})
-const timeFormatter = useTimeFormatter()
-const statusFormatter = useEnumFormatter(GameStatus)
-const table = ref(null)
-const model = ref({})
-const rules = {
-    name: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
-    type: [{ required: true, message: '请选择类型', trigger: 'blur' }],
-    roomId: [{ required: true, message: '请选择房间', trigger: 'blur' }],
-    autoReset: [{ required: true, message: '请输入自动重置', trigger: 'blur' }],
-    background: [{ required: true, message: '请输入故事背景', trigger: 'blur' }],
-    charactors: [
-        {
-            validator: (rule, value, callback) => {
-                if (!value || value.length === 0) {
-                    callback(new Error('请输入初始角色'))
-                } else {
-                    Promise.all(charactorForms.value.map((form) => form.validate()))
-                        .then(() => {
-                            callback()
-                        })
-                        .catch(() => {
-                            callback(new Error('请输入初始角色'))
-                        })
-                }
-            }
-        }
-    ]
-}
-
-const rooms = ref([])
-const promptTypes = ref([])
-onMounted(() => {
-    http.post('/room').then((res) => {
-        rooms.value = res.items
-    })
-    http.get('/prompt/types').then((res) => {
-        promptTypes.value = res
-    })
-})
-
-const { showEditDialog, onEdit } = setupEditDialog(model)
-async function submit() {
-    await http.put(model.value.id ? `/game/${model.value.id}` : '/game', model.value)
-    ElMessage.success('保存成功')
-}
-async function onDelete(row) {
-    await ElMessageBox.confirm('确认删除?', '删除', { type: 'warning' })
-    await http.delete(`/game/${row.id}`)
-    ElMessage.success('删除成功')
-    table.value.refresh()
-}
-const generating = ref(false)
-async function genCharactor() {
-    if (!model.value.background) {
-        ElMessage.error('请先输入故事背景')
-        return
-    }
-    if (!model.value.background) {
-        ElMessage.error('请先选择类型')
-        return
-    }
-    const { value } = await ElMessageBox.prompt('生成数量:', '生成角色', {
-        inputPattern: /^\d{1,2}$/,
-        inputErrorMessage: 'Invalid Num'
-    })
-    generating.value = true
-    try {
-        const res = await http.post(`/game/genCharactor`, {
-            type: model.value.type,
-            background: model.value.background,
-            num: value
-        })
-        generating.value = false
-        model.value.charactors = (model.value.charactors || []).concat(res)
-    } catch (error) {
-        ElMessage.error(error.message)
-        generating.value = false
-    }
-}
-function onAddCharactor() {
-    model.value.charactors = (model.value.charactors || []).concat({})
-}
-const charactorRules = {
-    avatar: [{ required: true, message: '请上传头像', trigger: 'blur' }],
-    name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
-    gender: [{ required: true, message: '请输入性别', trigger: 'blur' }],
-    age: [{ required: true, message: '请输入年龄', trigger: 'blur' }],
-    occupation: [{ required: true, message: '请输入职业', trigger: 'blur' }],
-    personality: [{ required: true, message: '请输入性格', trigger: 'blur' }],
-    background: [{ required: true, message: '请输入背景', trigger: 'blur' }]
-}
-const charactorForms = ref([])
-function removeCharactor(index) {
-    model.value.charactors.splice(index, 1)
-}
-function onPlay(row) {
-    router.push({
-        name: 'play',
-        params: {
-            gameId: row.id,
-            roomId: row.roomId
-        }
-    })
-}
-const showInitDialog = ref(false)
-const initForm = ref(null)
-const initModel = ref({})
-const selectedRow = ref(null)
-const initRules = {
-    date: [{ required: true, message: '请选择日期', trigger: 'blur' }],
-    time: [{ required: true, message: '请选择时间', trigger: 'blur' }]
-}
-async function onInit(row) {
-    selectedRow.value = row
-    initModel.value = {}
-    initForm.value?.clearValidate()
-    showInitDialog.value = true
-}
-async function init() {
-    await initForm.value.validate()
-    const loadingInstance = ElLoading.service({
-        fullscreen: true
-    })
-    try {
-        await http.post(`/game/${selectedRow.value.id}/init`, initModel.value)
-        ElMessage.success('初始化成功')
-        table.value.refresh()
-        showInitDialog.value = false
-    } catch (error) {
-        ElMessage.error(error.message)
-    }
-    loadingInstance.close()
-}
-async function onActiveChange(e, row) {
-    const loadingInstance = ElLoading.service({
-        fullscreen: true
-    })
-    try {
-        if (e) {
-            await http.post(`/game/${row.id}/startRun`)
-        } else {
-            await http.post(`/game/${row.id}/stopRun`)
-        }
-        table.value.refresh()
-    } catch (error) {
-        ElMessage.error(error.message)
-    }
-    loadingInstance.close()
-}
-async function reset(row) {
-    ElMessageBox.confirm('确认重置?', '重置', {
-        type: 'warning',
-        beforeClose: (action, instance, done) => {
-            if (action === 'confirm') {
-                instance.confirmButtonLoading = true
-                instance.confirmButtonText = '执行中...'
-                http.post(`/game/${row.id}/reset`)
-                    .then(() => {
-                        ElMessage.success('重置成功')
-                        table.value.refresh()
-                        done()
-                    })
-                    .catch((error) => {
-                        ElMessage.error(error.message)
-                        done()
-                    })
-            } else {
-                done()
-            }
-        }
-    })
-}
-</script>
-<style lang="less" scoped>
-.charactor-form {
-    background-color: var(--el-color-info-light-9);
-    padding: 15px;
-    border-radius: 8px;
-    margin-bottom: 10px;
-    .el-form-item {
-        margin-bottom: 20px;
-    }
-}
-</style>

+ 3 - 3
src/views/LoginView.vue

@@ -1,7 +1,7 @@
 <template>
     <ElContainer class="h-full bg-cover bg-center" @keyup.enter="login" :style="{ backgroundImage: `url(${bg})` }">
         <ElMain class="backdrop-blur1 dark:backdrop-brightness-50 !flex flex-col items-center justify-center">
-            <span class="text-3xl font-[sh] text-white [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">AIRPG</span>
+            <span class="text-3xl font-[sh] text-white [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">Admin</span>
             <ElCard class="w-full max-w-lg !rounded-xl mt-8 mb-16">
                 <ElForm :model="model" :rules="rules" label-position="top" ref="form">
                     <ElFormItem prop="username" label="用户名">
@@ -46,12 +46,12 @@ function login() {
         loading.value = true
         try {
             const res = await http.post('/auth/admin/login', model.value)
-            http.setToken(res.access_token)
+            http.setToken(res.token)
             router.replace({ name: 'home' })
             loading.value = false
         } catch (e) {
             loading.value = false
-            ElMessage.error(e.message)
+            ElMessage.error(e.errors?.[0].message || e.message)
         }
     })
 }

+ 6 - 28
src/views/MainView.vue

@@ -2,7 +2,7 @@
     <ElContainer class="h-full">
         <ElAside class="bg-slate-100 dark:bg-zinc-800" v-if="!isMobile">
             <div class="h-16 px-4 flex items-center justify-center cursor-pointer">
-                <span class="text-lg font-[sh]">AIRPG</span>
+                <span class="text-lg font-[sh]">Admin</span>
             </div>
             <SideMenu :default-active="activeMenu" :menus="menus" />
         </ElAside>
@@ -54,25 +54,13 @@ import DarkSwitch from '@/components/DarkSwitch.vue'
 import SideMenu from '@/components/SideMenu.vue'
 import { useRoute } from 'vue-router'
 import { ref, watch, shallowRef, inject } from 'vue'
-import {
-    User,
-    MoodSmile,
-    Wallet,
-    Home,
-    ExternalLink,
-    Menu2,
-    Settings,
-    Photo,
-    DeviceGamepad,
-    Prompt,
-    Camera
-} from '@vicons/tabler'
+import { User, Home, Menu2, Settings, Video } from '@vicons/tabler'
 import UserAvatar from '@/components/UserAvatar.vue'
 import ChangePwd from '@/components/ChangePwd.vue'
 import { http } from '@/plugins/http'
 
 const route = useRoute()
-const activeMenu = ref(route.path || '/home')
+const activeMenu = ref(route.name || '/home')
 const isMobile = inject('isMobile')
 const showDrawer = ref(false)
 const menus = [
@@ -82,19 +70,9 @@ const menus = [
         icon: Home
     },
     {
-        name: '/room',
-        title: '直播间',
-        icon: Camera
-    },
-    {
-        name: '/game',
-        title: '游戏管理',
-        icon: DeviceGamepad
-    },
-    {
-        name: '/prompt',
-        title: '提示词',
-        icon: Prompt
+        name: 'series',
+        title: '短剧列表',
+        icon: Video
     },
     {
         name: 'user-parent',

+ 0 - 347
src/views/PlayView.vue

@@ -1,347 +0,0 @@
-<template id="asdfasdf">
-    <!-- <div class="h-full overflow-auto p-4 rounded-lg bg-white dark:bg-neutral-800"> -->
-    <el-timeline>
-        <el-timeline-item
-            type="primary"
-            placement="top"
-            v-for="(item, index) in history"
-            :key="index"
-            :timestamp="formatDatetime(item.date, item.time)"
-        >
-            <ElCard>
-                <ElButton :icon="InfoCircle" size="small" type="info" plain @click="onDetail(item)"></ElButton>&nbsp;
-                <span class="whitespace-pre-wrap" v-html="item.plot"></span>
-                <template v-if="index === history.length - 1 && item.options && item.options.length">
-                    <br /><br />
-                    <el-radio-group v-model="choice" class="ml-4">
-                        <el-radio
-                            v-for="(c, i) in item.options"
-                            :key="i"
-                            :label="i"
-                            class="w-full !h-auto !whitespace-normal mb-4"
-                        >
-                            {{ c.content }}<br />
-                            <span>
-                                {{
-                                    c.modifyHp
-                                        .map((i) => `${i.name}${i.changeValue > 0 ? '+' : ''}${i.changeValue}`)
-                                        .join(', ')
-                                }}
-                            </span>
-                        </el-radio>
-                    </el-radio-group>
-                </template>
-                <template v-else-if="item.options && item.options.length">
-                    <br /><br />
-                    <el-radio-group :model-value="item.choiceIndex" disabled class="ml-4">
-                        <el-radio
-                            v-for="(c, i) in item.options"
-                            :key="i"
-                            :label="i"
-                            class="w-full !h-auto !whitespace-normal mb-4"
-                        >
-                            {{ c.content }}<br />
-                            <span>
-                                {{
-                                    c.modifyHp
-                                        .map((i) => `${i.name}${i.changeValue > 0 ? '+' : ''}${i.changeValue}`)
-                                        .join(', ')
-                                }}
-                            </span>
-                        </el-radio>
-                    </el-radio-group>
-                </template>
-                <template v-if="index === history.length - 1">
-                    <br />
-                </template>
-            </ElCard>
-        </el-timeline-item>
-    </el-timeline>
-    <div class="text-center">
-        <ElButtonGroup v-if="game.status === 'initialized'">
-            <ElButton :loading="loading" @click="continueGame(false)">继续</ElButton>
-            <ElButton :loading="loading" @click="onAddCharactor(false)">加入角色继续</ElButton>
-            <ElButton :loading="loading" @click="continueGame(true)">生成选择</ElButton>
-            <ElButton :loading="loading" @click="onAddCharactor(true)">加入角色生成选择</ElButton>
-            <ElButton :loading="loading" @click="revert">回退</ElButton>
-        </ElButtonGroup>
-        <ElButton v-if="game.status === 'finished'" :loading="loading" @click="revert">回退</ElButton>
-    </div>
-    <ElDialog title="详情" v-model="showDetailDialog" width="1000px">
-        <ElCollapse :model-value="['1', '2']">
-            <ElCollapseItem title="角色信息" name="1">
-                <ElTable :data="detail.charactors" size="small">
-                    <ElTableColumn prop="avatar" label="头像" width="50">
-                        <template #default="{ row }">
-                            <ElImage
-                                :src="row.avatar"
-                                fit="cover"
-                                :preview-src-list="[row.avatar]"
-                                style="width: 30px; height: 30px"
-                            ></ElImage>
-                        </template>
-                    </ElTableColumn>
-                    <ElTableColumn prop="name" label="角色名称" width="80"></ElTableColumn>
-                    <ElTableColumn prop="gender" label="性别" width="45" align="center"></ElTableColumn>
-                    <ElTableColumn prop="age" label="年龄" width="45" align="center"></ElTableColumn>
-                    <ElTableColumn prop="occupation" label="职业" width="80" show-overflow-tooltip></ElTableColumn>
-                    <ElTableColumn prop="personality" label="性格" show-overflow-tooltip></ElTableColumn>
-                    <ElTableColumn prop="background" label="背景" show-overflow-tooltip></ElTableColumn>
-                    <ElTableColumn prop="hp" label="生命值" width="55" align="center"></ElTableColumn>
-                    <ElTableColumn prop="status" label="状态" width="45" align="center">
-                        <template #default="{ row }">
-                            <span>{{ row.dead ? '死亡' : '存活' }}</span>
-                        </template>
-                    </ElTableColumn>
-                    <ElTableColumn prop="joinAt" label="加入时间" :formatter="timeFormatter" width="80"></ElTableColumn>
-                    <ElTableColumn prop="deadAt" label="死亡时间" :formatter="timeFormatter" width="80"></ElTableColumn>
-                </ElTable>
-            </ElCollapseItem>
-            <ElCollapseItem title="剧情概要" name="2">
-                {{ detail.summary }}
-            </ElCollapseItem>
-        </ElCollapse>
-    </ElDialog>
-    <ElDialog title="加入角色" v-model="showAddCharactorDialog" width="500px">
-        <ElForm :model="charactorModel" label-position="top" inline :rules="charactorRules" ref="charactorForm">
-            <ElFormItem prop="avatar" label="头像">
-                <SingleUpload v-model="charactorModel.avatar" />
-            </ElFormItem>
-            <ElFormItem prop="name" label="角色名称">
-                <ElInput v-model="charactorModel.name" />
-            </ElFormItem>
-            <ElFormItem prop="gender" label="性别">
-                <ElInput v-model="charactorModel.gender" />
-            </ElFormItem>
-            <ElFormItem prop="age" label="年龄">
-                <ElInput v-model="charactorModel.age" />
-            </ElFormItem>
-            <ElFormItem prop="occupation" label="职业">
-                <ElInput v-model="charactorModel.occupation" />
-            </ElFormItem>
-            <ElFormItem prop="personality" label="性格">
-                <ElInput v-model="charactorModel.personality" />
-            </ElFormItem>
-            <div class="w-full"></div>
-            <ElFormItem class="w-full" prop="background" label="背景">
-                <ElInput v-model="charactorModel.background" type="textarea" />
-            </ElFormItem>
-        </ElForm>
-        <template #footer>
-            <ElButton @click="showAddCharactorDialog = false">取消</ElButton>
-            <ElButton type="primary" @click="addCharactor">确定</ElButton>
-        </template>
-    </ElDialog>
-    <!-- </div> -->
-</template>
-<script setup>
-import { http } from '@/plugins/http'
-import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
-import { useRoute } from 'vue-router'
-import { format } from 'date-fns'
-import { zhCN } from 'date-fns/locale'
-import { ElMessage, ElMessageBox } from 'element-plus'
-import { InfoCircle } from '@vicons/tabler'
-import { useTimeFormatter } from '@/utils/formatter'
-import SingleUpload from '@/components/SingleUpload.vue'
-import { io } from 'socket.io-client'
-
-const route = useRoute()
-const roomId = route.params.roomId
-const gameId = route.params.gameId
-const game = ref({})
-const history = ref([])
-const choice = ref(-1)
-
-const needChoice = computed(() => {
-    return (history.value[history.value.length - 1].options || []).length > 0
-})
-async function getData(params) {
-    http.get(`/game/${gameId}`, { params }).then((res) => {
-        game.value = res
-    })
-    http.get(`/game/${gameId}/history`, { params }).then((res) => {
-        res.forEach((i) => {
-            if (i.options && i.choice) {
-                i.choiceIndex = i.options.findIndex((o) => o.content === i.choice.content)
-            }
-        })
-        const last = res[res.length - 1]
-        if (last && last.options && last.options.length && last.choice) {
-            if (
-                history.value.length === 0 ||
-                (res[res.length - 1] &&
-                    history.value[history.value.length - 1] &&
-                    res[res.length - 1].id !== history.value[history.value.length - 1].id)
-            ) {
-                choice.value = last.options.findIndex((i) => i.content === last.choice.content)
-            }
-        }
-
-        history.value = res
-        setTimeout(() => {
-            document
-                .querySelector('#main-container')
-                .scrollTo({ top: document.querySelector('#main-container').scrollHeight, behavior: 'smooth' })
-        }, 500)
-    })
-}
-getData()
-const socket = ref(null)
-onMounted(() => {
-    socket.value = io(import.meta.env.VITE_WS_URL)
-    socket.value.on('connect', () => {
-        console.log('[websocket] connected')
-    })
-    socket.value.on('disconnect', () => {
-        console.log('[websocket] disconnected')
-    })
-    socket.value.on(`${gameId}`, (...args) => {
-        console.log('[websocket] received message', args[0])
-        const data = args[0]
-        switch (data.type) {
-            case 'timeChange':
-                if (history.value[history.value.length - 1].id !== -1) {
-                    history.value.push({
-                        id: -1
-                    })
-                }
-                history.value[history.value.length - 1] = {
-                    ...history.value[history.value.length - 1],
-                    ...data.data
-                }
-                break
-            case 'plot':
-                if (history.value[history.value.length - 1].id !== -1) {
-                    history.value.push({
-                        id: -1
-                    })
-                }
-                history.value[history.value.length - 1] = {
-                    ...history.value[history.value.length - 1],
-                    plot: data.data
-                }
-                // // 创建一个新的SpeechSynthesisUtterance对象
-                // var utterance = new SpeechSynthesisUtterance()
-                // // 语速
-                // utterance.rate = 1.5;
-                // // 设置要合成的文本
-                // utterance.text = data.data
-
-                // // 获取语音合成对象
-                // var synth = window.speechSynthesis
-
-                // // 添加要合成的文本到队列中
-                // synth.speak(utterance)
-                break
-            case 'options':
-                if (history.value[history.value.length - 1].id !== -1) {
-                    history.value.push({
-                        id: -1
-                    })
-                }
-                history.value[history.value.length - 1] = {
-                    ...history.value[history.value.length - 1],
-                    options: data.data
-                }
-                break
-            case 'state':
-                getData()
-                break
-        }
-        if (data.type !== 'votes') {
-            setTimeout(() => {
-                document
-                    .querySelector('#main-container')
-                    .scrollTo({ top: document.querySelector('#main-container').scrollHeight, behavior: 'smooth' })
-            }, 500)
-        }
-    })
-    socket.value.connect()
-})
-onBeforeUnmount(() => {
-    socket.value?.disconnect()
-})
-function formatDatetime(date, time) {
-    try {
-        return (
-            format(new Date(date), 'MMMdo', { locale: zhCN }) +
-            {
-                morning: '上午',
-                afternoon: '下午',
-                evening: '晚上'
-            }[time]
-        )
-    } catch (error) {
-        return ''
-    }
-}
-const loading = ref(false)
-
-async function continueGame(genChoice = false, newCharactor = null) {
-    if (needChoice.value && choice.value < 0) {
-        ElMessage.info('请选择一个选项')
-        return
-    }
-    loading.value = true
-    try {
-        await http.post(`/game/${gameId}/continue`, {
-            genChoice: genChoice,
-            choice: history.value[history.value.length - 1].options[choice.value],
-            newCharactor: newCharactor
-        })
-        loading.value = false
-        getData()
-    } catch (error) {
-        ElMessage.error(error.message)
-        loading.value = false
-    }
-}
-
-async function revert() {
-    loading.value = true
-    try {
-        await http.post(`/game/${gameId}/revert`)
-    } catch (error) {
-        ElMessage.error(error.message)
-    }
-    loading.value = false
-    getData()
-}
-
-const showAddCharactorDialog = ref(false)
-const charactorRules = {
-    avatar: [{ required: true, message: '请上传头像', trigger: 'blur' }],
-    name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
-    gender: [{ required: true, message: '请输入性别', trigger: 'blur' }],
-    age: [{ required: true, message: '请输入年龄', trigger: 'blur' }],
-    occupation: [{ required: true, message: '请输入职业', trigger: 'blur' }],
-    personality: [{ required: true, message: '请输入性格', trigger: 'blur' }],
-    background: [{ required: true, message: '请输入背景', trigger: 'blur' }]
-}
-const charactorForm = ref(null)
-const charactorModel = ref({})
-const genChoice = ref(false)
-
-async function onAddCharactor(g = false) {
-    charactorModel.value = {}
-    charactorForm.value?.clearValidate()
-    showAddCharactorDialog.value = true
-    genChoice.value = g
-}
-
-async function addCharactor() {
-    await charactorForm.value.validate()
-    showAddCharactorDialog.value = false
-    continueGame(genChoice.value, charactorModel.value)
-}
-
-const showDetailDialog = ref(false)
-const detail = ref({})
-function onDetail(row) {
-    detail.value = { ...row }
-    showDetailDialog.value = true
-}
-const timeFormatter = useTimeFormatter('MM/dd')
-</script>
-<style lang="less" scoped></style>

+ 0 - 257
src/views/PromptView.vue

@@ -1,257 +0,0 @@
-<template>
-    <div>
-        <div class="">
-            <ElSelect v-model="type">
-                <ElOption v-for="item in types" :key="item" :label="item" :value="item" />
-            </ElSelect>
-            <ElButton :icon="Plus" class="ml-4" @click="addType"></ElButton>
-            <ElButton :icon="Trash" @click="delType" />
-        </div>
-        <el-tabs v-model="activeName" class="mt-4" tabPosition="left">
-            <el-tab-pane v-for="(item, i) in prompts" :key="item.name" :label="item.description" :name="item.name">
-                <ElInput v-model="item.template" type="textarea" :autosize="{ min: 20 }"></ElInput>
-                <div class="mt-4">
-                    <ElButton @click="save(i)" :loading="loading" type="primary">保存</ElButton>
-                    <ElButton @click="test(i)" :loading="loading">测试</ElButton>
-                    <ElButton @click="restore(i)" :disabled="loading">恢复默认</ElButton>
-                </div>
-            </el-tab-pane>
-        </el-tabs>
-        <br />
-        <br />
-        <div class="ml-32 flex text-sm text-neutral-600 dark:text-neutral-400">
-            <div class="">
-                可用变量:<br />
-                days: 运行天数<br />
-                charactors: 角色列表<br />
-                total: 总人数<br />
-                alive: 存活人数<br />
-                dead: 死亡人数<br />
-                minHp: 最低生命值<br />
-                maxHp: 最高生命值<br />
-                avgHp: 平均生命值<br /><br />
-                语法示例:
-                <code class="bg-neutral-300 dark:bg-neutral-700 px-2 py-1 rounded-sm"
-                    >&lt;% return alive > 5 ? '加入战斗情节' : '' %&gt;</code
-                ><br />
-                <br />
-            </div>
-            <div class="pl-8">
-                测试变量:<br />
-                <json-viewer :value="testVarsFinal" :expand-depth="1" copyable boxed sort></json-viewer>
-            </div>
-        </div>
-    </div>
-</template>
-<script setup>
-import { http } from '@/plugins/http'
-import { onMounted, ref, watch } from 'vue'
-import { ElMessage } from 'element-plus'
-import { Plus, Trash } from '@vicons/tabler'
-import { ElMessageBox } from 'element-plus'
-import JsonViewer from 'vue-json-viewer'
-import {} from '@vicons/tabler'
-const activeName = ref('')
-const prompts = ref([])
-const types = ref([])
-const type = ref(null)
-const props = defineProps({
-    gameId: {
-        type: Number,
-        default: 0
-    }
-})
-function load() {
-    http.get('/prompt/types').then((res) => {
-        types.value = res
-        if (res.length === 0) {
-            type.value = null
-            prompts.value = []
-        } else {
-            if (!type.value) {
-                type.value = res[0]
-            } else if (!res.find((item) => item === type.value)) {
-                type.value = res[0]
-            }
-        }
-    })
-}
-watch(type, (val) => {
-    if (val) {
-        http.get(`/prompt/${val}`).then((res) => {
-            prompts.value = res
-        })
-    }
-})
-onMounted(() => {
-    load()
-})
-const loading = ref(false)
-async function save(i) {
-    if (!test(i, false)) {
-        return
-    }
-    try {
-        loading.value = true
-        await http.put('/prompt', prompts.value[i])
-        loading.value = false
-        ElMessage.success('保存成功')
-    } catch (error) {
-        loading.value = false
-        ElMessage.error(error.message)
-    }
-}
-async function restore(i) {
-    prompts.value[i].template = prompts.value[i].defaultTemplate
-}
-async function addType() {
-    const { value: type } = await ElMessageBox.prompt('请输入类型', '添加类型', {
-        inputPattern: /.+/,
-        inputErrorMessage: '请输入类型'
-    })
-    console.log(type)
-    try {
-        await http.put(`/prompt/types`, { type })
-        ElMessage.success('添加成功')
-        load()
-    } catch (error) {
-        ElMessage.error(error.message)
-    }
-}
-async function delType() {
-    await ElMessageBox.confirm('确定删除该类型吗?', '删除类型', {
-        confirmButtonText: '确定',
-        cancelButtonText: '取消',
-        type: 'warning'
-    })
-    try {
-        await http.delete(`/prompt/${type.value}`)
-        ElMessage.success('删除成功')
-        load()
-    } catch (error) {
-        ElMessage.error(error.message)
-    }
-}
-const testVars = {
-    days: 4,
-    charactors: [
-        {
-            name: '约翰',
-            gender: '男',
-            age: '35',
-            occupation: '侦探',
-            personality: '1号',
-            background: '前警探,以侦破神秘犯罪而闻名',
-            hp: 0,
-            joinAt: '2023-10-10T16:00:00.000Z',
-            survival: 0,
-            dead: true,
-            avatar: 'https://nebuai.oss-cn-hangzhou.aliyuncs.com/image/20231012/7laitqdb.png',
-            deadAt: '2023-10-15T16:00:00.000Z'
-        },
-        {
-            name: '艾米丽',
-            gender: '女',
-            age: '28',
-            occupation: '记者',
-            personality: '好奇心重',
-            background: '调查性记者,专注于揭露黑暗的秘密。',
-            hp: 0,
-            joinAt: '2023-10-10T16:00:00.000Z',
-            survival: 0,
-            dead: true,
-            avatar: 'https://nebuai.oss-cn-hangzhou.aliyuncs.com/image/20231012/dgg7bxc6.png',
-            deadAt: '2023-10-14T16:00:00.000Z'
-        },
-        {
-            name: '威廉',
-            gender: '女',
-            age: '45',
-            occupation: '律师',
-            personality: '聪明的',
-            background: '非常成功的辩护律师,以为自己的客户获得无罪声誉而闻名。',
-            hp: 10,
-            joinAt: '2023-10-10T16:00:00.000Z',
-            survival: 0,
-            dead: false,
-            avatar: 'https://nebuai.oss-cn-hangzhou.aliyuncs.com/image/20231012/slxskike.png'
-        },
-        {
-            name: '伊莎贝拉',
-            gender: '女',
-            age: '30',
-            occupation: '法医',
-            personality: '善于分析',
-            background: '具有DNA分析专业知识的法医科学家,解决了许多冷案。',
-            hp: 0,
-            joinAt: '2023-10-10T16:00:00.000Z',
-            survival: 0,
-            dead: true,
-            avatar: 'https://nebuai.oss-cn-hangzhou.aliyuncs.com/image/20231012/oqqliuus.png',
-            deadAt: '2023-10-14T16:00:00.000Z'
-        },
-        {
-            name: '亚历山大',
-            gender: '男',
-            age: '40',
-            occupation: '心理学家',
-            personality: '富有洞察力的',
-            background: '著名心理学家,专长是犯罪心理分析。',
-            hp: 10,
-            joinAt: '2023-10-10T16:00:00.000Z',
-            survival: 0,
-            dead: false,
-            avatar: 'https://nebuai.oss-cn-hangzhou.aliyuncs.com/image/20231012/hqflwm6u.png'
-        }
-    ]
-}
-function evalPrompt(vars, template) {
-    vars.total = vars.charactors.length
-    vars.alive = vars.charactors.filter((i) => !i.dead).length
-    vars.dead = vars.charactors.filter((i) => i.dead).length
-    const hps = vars.charactors.filter((i) => i.hp > 0).map((i) => i.hp)
-    vars.minHp = hps.length > 0 ? Math.min(...hps) : 0
-    vars.maxHp = hps.length > 0 ? Math.max(...hps) : 0
-    vars.avgHp = hps.length > 0 ? hps.reduce((a, b) => a + b) / hps.length : 0
-    const reg = /<%([^%]*(?:%(?!>)[^%]*)*)%>/
-    while (reg.test(template)) {
-        template = template.replace(reg, function (match, $1) {
-            var func = new Function('days', 'charactors', 'total', 'alive', 'dead', 'minHp', 'maxHp', 'avgHp', $1)
-            return func(
-                vars.days,
-                vars.charactors,
-                vars.total,
-                vars.alive,
-                vars.dead,
-                vars.minHp,
-                vars.maxHp,
-                vars.avgHp
-            )
-        })
-    }
-    return {
-        template,
-        vars
-    }
-}
-const testVarsFinal = evalPrompt(testVars, '<% return alive > 5 ? "加入战斗情节" : "" %>').vars
-function test(i, showResult = true) {
-    try {
-        const result = evalPrompt(testVars, prompts.value[i].template)
-        if (showResult) {
-            ElMessageBox.alert(`<p class="whitespace-pre-wrap">${result.template}</p>`, `测试结果`, {
-                dangerouslyUseHTMLString: true,
-                customStyle: { maxWidth: '800px' }
-            })
-        }
-        return true
-    } catch (error) {
-        console.log(error)
-        ElMessageBox.alert(`<p class="text-red-500">${error.message}</p>`, '测试失败', {
-            type: 'error',
-            dangerouslyUseHTMLString: true
-        })
-    }
-    return false
-}
-</script>

+ 0 - 80
src/views/RoomView.vue

@@ -1,80 +0,0 @@
-<template>
-    <PagingTable url="/room" :where="where" ref="table">
-        <template #filter>
-            <ElButton :icon="Plus" @click="onEdit()">添加</ElButton>
-        </template>
-        <ElTableColumn prop="id" label="#" width="80" />
-        <ElTableColumn prop="name" label="名称" />
-        <ElTableColumn prop="platform" label="平台" :formatter="platformFormatter" />
-        <ElTableColumn prop="channelId" label="频道" />
-        <ElTableColumn prop="currentGameId" label="当前游戏" />
-        <ElTableColumn prop="active" label="直播中" width="100" align="center">
-            <template #default="{ row }">
-                <ElSwitch v-model="row.active" @change="onActiveChange($event, row)" />
-            </template>
-        </ElTableColumn>
-        <ElTableColumn prop="notice" label="公告" show-overflow-tooltip />
-        <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
-        <ElTableColumn label="操作" align="center" width="120">
-            <template #default="{ row }">
-                <ElButton @click="onEdit(row)">编辑</ElButton>
-            </template>
-        </ElTableColumn>
-    </PagingTable>
-    <EditDialog v-model="showEditDialog" :model="model" :rules="rules" :on-submit="submit" @success="table.refresh()">
-        <ElFormItem prop="name" label="名称">
-            <ElInput v-model="model.name" placeholder="请输入名称" />
-        </ElFormItem>
-        <ElFormItem prop="platform" label="平台">
-            <EnumSelect :enum="StreamPlatform" v-model="model.platform" />
-        </ElFormItem>
-        <ElFormItem prop="channelId" label="频道">
-            <ElInput v-model="model.channelId" placeholder="请输入频道" />
-        </ElFormItem>
-        <ElFormItem prop="code" label="身份码" v-if="model.platform === 'bilibili'">
-            <ElInput v-model="model.code" placeholder="请输入身份码" />
-        </ElFormItem>
-        <ElFormItem prop="notice" label="公告">
-            <ElInput v-model="model.notice" placeholder="请输入公告" />
-        </ElFormItem>
-    </EditDialog>
-</template>
-<script setup>
-import { onActivated, ref } from 'vue'
-import PagingTable from '@/components/PagingTable.vue'
-import { useEnumFormatter, useTimeFormatter } from '@/utils/formatter'
-import { Plus } from '@vicons/tabler'
-import EditDialog from '@/components/EditDialog.vue'
-import { setupEditDialog } from '@/utils/editDialog'
-import EnumSelect from '@/components/EnumSelect.vue'
-import { UserRole } from '@/enums'
-import { http } from '@/plugins/http'
-import { ElMessage } from 'element-plus'
-import { useClipboard } from '@vueuse/core'
-import { StreamPlatform } from '@/enums'
-
-const where = ref({})
-const timeFormatter = useTimeFormatter()
-const platformFormatter = useEnumFormatter(StreamPlatform)
-const table = ref(null)
-const model = ref({})
-const rules = {
-    name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
-    platform: [{ required: true, message: '请选择平台', trigger: 'blur' }],
-    channelId: [{ required: true, message: '请输入频道', trigger: 'blur' }],
-    code: [{ required: true, message: '请输入身份码', trigger: 'blur' }]
-}
-const { showEditDialog, onEdit } = setupEditDialog(model)
-async function submit() {
-    await http.put('/room', model.value)
-    ElMessage.success('保存成功')
-}
-function onActiveChange(e, row) {
-    console.log(e, row)
-    if (e) {
-        http.post(`/room/${row.id}/start`)
-    } else {
-        http.post(`/room/${row.id}/stop`)
-    }
-}
-</script>

+ 77 - 0
src/views/SeriesView.vue

@@ -0,0 +1,77 @@
+<template>
+    <PagingTable url="/series" :query="query" ref="table">
+        <template #filter>
+            <ElButton :icon="Plus" @click="onEdit()">添加</ElButton>
+        </template>
+        <ElTableColumn prop="id" label="#" width="80" />
+        <ElTableColumn prop="title" label="剧名" min-width="120" />
+        <ElTableColumn prop="cover" label="封面" width="120" align="center">
+            <template #default="{ row }">
+                <SecureImage style="width: 30px; height: 35px; vertical-align: middle" :src="row.cover" fit="cover" />
+            </template>
+        </ElTableColumn>
+        <ElTableColumn prop="price" label="价格" width="150" />
+        <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
+        <ElTableColumn label="操作" align="center" width="120">
+            <template #default="{ row }">
+                <ElButton @click="onEdit(row)">编辑</ElButton>
+            </template>
+        </ElTableColumn>
+    </PagingTable>
+    <EditDialog v-model="showEditDialog" :model="model" :rules="rules" :on-submit="submit" @success="table.refresh()">
+        <ElFormItem prop="title" label="剧名">
+            <ElInput v-model="model.title" placeholder="请输入剧名" />
+        </ElFormItem>
+        <ElFormItem prop="description" label="描述">
+            <ElInput v-model="model.description" placeholder="请输入描述" />
+        </ElFormItem>
+        <ElFormItem prop="cover" label="封面">
+            <SingleUpload v-model="model.cover" />
+        </ElFormItem>
+        <ElFormItem prop="price" label="价格">
+            <ElInput v-model="model.price" placeholder="请输入价格" />
+        </ElFormItem>
+        <ElFormItem prop="categories" label="分类">
+            <ElSelect v-model="model.categories" value-key="id" multiple>
+                <ElOption v-for="item in categories" :key="item.id" :label="item.name" :value="item"/>
+            </ElSelect>
+        </ElFormItem>
+    </EditDialog>
+</template>
+<script setup>
+import { onMounted, reactive, ref } from 'vue'
+import PagingTable from '@/components/PagingTable.vue'
+import { useTimeFormatter } from '@/utils/formatter'
+import { Plus } from '@vicons/tabler'
+import EditDialog from '@/components/EditDialog.vue'
+import { setupEditDialog } from '@/utils/editDialog'
+import EnumSelect from '@/components/EnumSelect.vue'
+import { http } from '@/plugins/http'
+import { ElMessage } from 'element-plus'
+import { useClipboard } from '@vueuse/core'
+import SingleUpload from '@/components/SingleUpload.vue'
+const query = ref({
+    preload: 'categories',
+    categories: 1
+})
+const timeFormatter = useTimeFormatter()
+const table = ref(null)
+const model = ref({})
+const rules = {
+    title: [{ required: true, message: '请输入剧名', trigger: 'blur' }],
+    cover: [{ required: true, message: '请上传封面', trigger: 'blur' }]
+}
+const { showEditDialog } = setupEditDialog(model)
+async function onEdit(row) {
+    model.value = row ? await http.get(`/series/${row.id}`) : {}
+    showEditDialog.value = true
+}
+async function submit() {
+    await http.put(model.value.id ? `/series/${model.value.id}` : '/series', model.value)
+    ElMessage.success('保存成功')
+}
+const categories = ref([])
+onMounted(async () => {
+    categories.value = (await http.get('/categories', { pageSize: 1000 })).data
+})
+</script>

+ 3 - 3
src/views/UserView.vue

@@ -1,7 +1,7 @@
 <template>
-    <PagingTable url="/admin/users" :where="where" ref="table">
+    <PagingTable url="/users" :query="query" ref="table">
         <template #filter>
-            <EnumSelect :enum="UserRole" v-model="where.roles"></EnumSelect>
+            <EnumSelect :enum="UserRole" v-model="query.roles"></EnumSelect>
             <ElButton :icon="Plus" @click="onEdit()">添加</ElButton>
         </template>
         <ElTableColumn prop="id" label="#" width="80" />
@@ -47,7 +47,7 @@ import { http } from '@/plugins/http'
 import { ElMessage } from 'element-plus'
 import { useClipboard } from '@vueuse/core'
 
-const where = ref({ roles: 'user' })
+const query = ref({ roles: 'user' })
 const timeFormatter = useTimeFormatter()
 const table = ref(null)
 const model = ref({})

Файловите разлики са ограничени, защото са твърде много
+ 1044 - 0
yarn.lock


Някои файлове не бяха показани, защото твърде много файлове са промени