feat: initial commit of AI Comic Generator

This commit is contained in:
p
2026-01-17 15:26:14 +08:00
commit 06d76e0e69
278 changed files with 8612 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
<template>
<el-config-provider :locale="locale">
<el-container>
<el-header class="main-header">
<div class="logo">AI Comic Generator</div>
<el-menu mode="horizontal" router :default-active="$route.path" class="nav-menu">
<el-menu-item index="/">Projects</el-menu-item>
<el-menu-item index="/config">Models</el-menu-item>
</el-menu>
</el-header>
<el-main>
<router-view />
</el-main>
</el-container>
</el-config-provider>
</template>
<script setup>
import { ref } from 'vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
const locale = ref(zhCn)
</script>
<style scoped>
.main-header {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #333;
background-color: #1e1e1e;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
color: #409EFF;
}
.nav-menu {
border-bottom: none;
background-color: transparent;
width: 300px;
}
</style>
+21
View File
@@ -0,0 +1,21 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
import './style.css'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.mount('#app')
+27
View File
@@ -0,0 +1,27 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import ConfigView from '../views/ConfigView.vue'
import ProjectView from '../views/ProjectView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/config',
name: 'config',
component: ConfigView
},
{
path: '/project/:id',
name: 'project',
component: ProjectView
}
]
})
export default router
+33
View File
@@ -0,0 +1,33 @@
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
background-color: #121212;
color: #e0e0e0;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
}
#app {
height: 100%;
}
.el-container {
height: 100%;
}
/* Scrollbar styling for tech look */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1e1e1e;
}
::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
+141
View File
@@ -0,0 +1,141 @@
<template>
<div class="config-view">
<div class="header">
<h2>Model Configurations</h2>
<el-button type="primary" @click="openDialog()">Add Config</el-button>
</div>
<el-table :data="configs" style="width: 100%">
<el-table-column prop="provider" label="Provider" />
<el-table-column prop="model_name" label="Model Name" />
<el-table-column prop="model_type" label="Type" />
<el-table-column prop="is_active" label="Active">
<template #default="scope">
<el-tag :type="scope.row.is_active ? 'success' : 'info'">{{ scope.row.is_active ? 'Yes' : 'No' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="Actions">
<template #default="scope">
<el-button size="small" @click="openDialog(scope.row)">Edit</el-button>
<el-button size="small" type="danger" @click="deleteConfig(scope.row.id)">Delete</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dialogVisible" :title="isEdit ? 'Edit Config' : 'Add Config'">
<el-form :model="form" label-width="120px">
<el-form-item label="Provider">
<el-select v-model="form.provider" placeholder="Select provider">
<el-option label="Google" value="google" />
</el-select>
</el-form-item>
<el-form-item label="Model Name">
<el-input v-model="form.model_name" placeholder="e.g. gemini-1.5-flash" />
</el-form-item>
<el-form-item label="API Key">
<el-input v-model="form.api_key" type="password" show-password />
</el-form-item>
<el-form-item label="Type">
<el-select v-model="form.model_type">
<el-option label="Text" value="text" />
<el-option label="Image" value="image" />
</el-select>
</el-form-item>
<el-form-item label="Base URL">
<el-input v-model="form.base_url" placeholder="Optional" />
</el-form-item>
<el-form-item label="Active">
<el-switch v-model="form.is_active" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="saveConfig">Save</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { ElMessage } from 'element-plus'
const configs = ref([])
const dialogVisible = ref(false)
const isEdit = ref(false)
const form = ref({
provider: 'google',
model_name: '',
api_key: '',
model_type: 'text',
base_url: '',
is_active: true
})
const fetchConfigs = async () => {
try {
const res = await axios.get('/api/v1/configs/')
configs.value = res.data
} catch (error) {
ElMessage.error('Failed to fetch configs')
}
}
const openDialog = (row = null) => {
if (row) {
isEdit.value = true
form.value = { ...row }
} else {
isEdit.value = false
form.value = {
provider: 'google',
model_name: '',
api_key: '',
model_type: 'text',
base_url: '',
is_active: true
}
}
dialogVisible.value = true
}
const saveConfig = async () => {
try {
if (isEdit.value) {
await axios.put(`/api/v1/configs/${form.value.id}`, form.value)
} else {
await axios.post('/api/v1/configs/', form.value)
}
ElMessage.success('Saved successfully')
dialogVisible.value = false
fetchConfigs()
} catch (error) {
ElMessage.error('Failed to save config')
}
}
const deleteConfig = async (id) => {
try {
await axios.delete(`/api/v1/configs/${id}`)
ElMessage.success('Deleted successfully')
fetchConfigs()
} catch (error) {
ElMessage.error('Failed to delete config')
}
}
onMounted(fetchConfigs)
</script>
<style scoped>
.config-view {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
</style>
+109
View File
@@ -0,0 +1,109 @@
<template>
<div class="home-view">
<div class="header">
<h2>My Projects</h2>
<el-button type="primary" @click="dialogVisible = true">New Project</el-button>
</div>
<el-row :gutter="20">
<el-col :span="6" v-for="project in projects" :key="project.id">
<el-card shadow="hover" class="project-card" @click="goToProject(project.id)">
<template #header>
<div class="card-header">
<span>{{ project.title }}</span>
<el-button type="text" icon="Delete" @click.stop="deleteProject(project.id)"></el-button>
</div>
</template>
<p>{{ project.description || 'No description' }}</p>
<div class="footer">
<span>{{ new Date(project.updated_at).toLocaleDateString() }}</span>
</div>
</el-card>
</el-col>
</el-row>
<el-dialog v-model="dialogVisible" title="Create New Project">
<el-form :model="form">
<el-form-item label="Title">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="Description">
<el-input type="textarea" v-model="form.description" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="createProject">Create</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
const router = useRouter()
const projects = ref([])
const dialogVisible = ref(false)
const form = ref({ title: '', description: '' })
const fetchProjects = async () => {
try {
const res = await axios.get('/api/v1/projects/')
projects.value = res.data
} catch (error) {
ElMessage.error('Failed to fetch projects')
}
}
const createProject = async () => {
try {
const res = await axios.post('/api/v1/projects/', form.value)
ElMessage.success('Created successfully')
dialogVisible.value = false
goToProject(res.data.id)
} catch (error) {
ElMessage.error('Failed to create project')
}
}
const deleteProject = async (id) => {
try {
await axios.delete(`/api/v1/projects/${id}`)
ElMessage.success('Deleted successfully')
fetchProjects()
} catch (error) {
ElMessage.error('Failed to delete project')
}
}
const goToProject = (id) => {
router.push(`/project/${id}`)
}
onMounted(fetchProjects)
</script>
<style scoped>
.home-view {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.project-card {
cursor: pointer;
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
File diff suppressed because it is too large Load Diff