feat: 完善 Prompt 管理与生成流程配置

This commit is contained in:
王鹏
2026-07-15 17:31:13 +08:00
parent 84d3c8fc05
commit de98ea59b1
93 changed files with 3474 additions and 980 deletions

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console" />
</root>
</configuration>

View File

@@ -0,0 +1 @@
20464

View File

@@ -0,0 +1 @@
33372

View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
"@vue/cli-plugin-babel/preset"
]
}

View File

@@ -0,0 +1,24 @@
{
"name": "product-web",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vue-cli-service serve --host 0.0.0.0",
"serve": "vue-cli-service serve --host 0.0.0.0",
"build": "vue-cli-service build"
},
"dependencies": {
"axios": "^0.27.2",
"core-js": "^3.8.3",
"echarts": "5.4.0",
"element-ui": "^2.15.14",
"quill": "^1.3.7",
"vue": "^2.6.14",
"vue-quill-editor": "^3.0.6",
"vue-router": "^3.6.5"
},
"devDependencies": {
"@vue/cli-service": "^4.5.19",
"vue-template-compiler": "^2.6.14"
}
}

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>演示商品</title>
</head>
<body>
<noscript>请启用 JavaScript 后访问。</noscript>
<div id="app"></div>
</body>
</html>

View File

@@ -0,0 +1,543 @@
<template>
<div class="portal-shell">
<template v-if="!isLoginRoute">
<header class="top-nav">
<router-link class="brand" to="/demo/product">
<span class="brand-logo" aria-hidden="true"><i></i></span>
<span>演示商品</span>
</router-link>
<nav class="portal-nav" aria-label="主导航">
<div class="portal-nav-group">
<router-link class="portal-nav-parent" to="/demo/product" exact>首页</router-link>
</div>
</nav>
<div class="user-area">
<template v-if="currentUser">
<button class="user-chip" type="button">
<span class="avatar">{{ userInitial }}</span>
<span>{{ currentUserName }}</span>
</button>
<button class="logout-link" type="button" @click="logout">退出</button>
</template>
<button v-else class="login-link" type="button" @click="goLogin">登录</button>
</div>
</header>
<section v-if="isHomeRoute" class="hero-banner" aria-label="项目横幅">
<div class="hero-pattern" aria-hidden="true"></div>
<div class="hero-content">
<span class="hero-eyebrow">WELCOME</span>
<h1>演示商品</h1>
<p>发现内容连接服务轻松完成每一次访问</p>
</div>
</section>
</template>
<main :class="['portal-main', { 'portal-main-login': isLoginRoute }]">
<router-view />
</main>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
currentUser: null
}
},
computed: {
isLoginRoute() {
return this.$route.path === "/login"
},
isHomeRoute() {
return this.$route.path === "/demo/product"
},
currentUserName() {
return this.currentUser && (this.currentUser.displayName || this.currentUser.account || this.currentUser.nickname || this.currentUser.username || "User")
},
userInitial() {
const name = this.currentUserName || "U"
return name.slice(0, 1).toUpperCase()
}
},
mounted() {
this.loadUser()
window.addEventListener("portal-user-change", this.loadUser)
},
beforeDestroy() {
window.removeEventListener("portal-user-change", this.loadUser)
},
methods: {
canShowMenu(visibleRoles) {
if (!Array.isArray(visibleRoles) || visibleRoles.length === 0) {
return true
}
const roleCode = this.currentUser && this.currentUser.roleCode
return Boolean(roleCode) && visibleRoles.indexOf(roleCode) !== -1
},
isNavGroupActive(paths) {
return Array.isArray(paths) && paths.indexOf(this.$route.path) !== -1
},
loadUser() {
const raw = localStorage.getItem("portal-user")
if (!raw) {
this.currentUser = null
return
}
try {
this.currentUser = JSON.parse(raw)
} catch (error) {
localStorage.removeItem("portal-user")
this.currentUser = null
}
},
goLogin() {
this.$router.push({
path: "/login",
query: { redirect: this.$route.fullPath }
})
},
logout() {
this.request.post("/auth/logout").catch(() => {})
localStorage.removeItem("portal-token")
localStorage.removeItem("portal-user")
window.dispatchEvent(new Event("portal-user-change"))
this.$router.push("/login")
}
}
}
</script>
<style>
:root {
--portal-primary: #167d83;
--portal-primary-strong: #11656a;
--portal-primary-soft: #eaf7f7;
--portal-accent: #2f72d6;
--portal-text: #172033;
--portal-text-secondary: #5f6b7a;
--portal-border: #e3e9f1;
--portal-surface: #ffffff;
--portal-background: #f5f7fb;
--portal-radius-sm: 8px;
--portal-radius-md: 12px;
--portal-radius-lg: 18px;
--portal-shadow-sm: 0 8px 24px rgba(15, 23, 42, 0.06);
--portal-shadow-md: 0 18px 44px rgba(15, 23, 42, 0.12);
--portal-content-width: 1200px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--portal-background);
color: var(--portal-text);
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
a {
color: inherit;
text-decoration: none;
}
button {
font: inherit;
}
button:focus-visible,
a:focus-visible {
outline: 3px solid rgba(47, 114, 214, 0.28);
outline-offset: 2px;
}
.portal-shell {
min-height: 100vh;
background: var(--portal-background);
}
.top-nav {
position: sticky;
top: 0;
z-index: 20;
display: grid;
grid-template-columns: minmax(180px, auto) minmax(0, 1fr) auto;
align-items: center;
gap: 24px;
min-height: 72px;
padding: 0 32px;
color: var(--portal-text);
background: rgba(255, 255, 255, 0.96);
border-bottom: 1px solid var(--portal-border);
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
backdrop-filter: blur(14px);
}
.brand,
.portal-nav,
.user-area,
.user-chip {
display: inline-flex;
align-items: center;
}
.brand {
gap: 10px;
min-width: 0;
font-size: 18px;
font-weight: 700;
}
.brand span:last-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.brand-logo {
position: relative;
display: inline-block;
width: 38px;
height: 38px;
flex: 0 0 38px;
place-items: center;
overflow: hidden;
border-radius: 11px;
color: #ffffff;
background: linear-gradient(135deg, var(--portal-primary), var(--portal-accent));
box-shadow: 0 7px 18px rgba(22, 125, 131, 0.24);
}
.brand-logo::before,
.brand-logo::after,
.brand-logo i {
position: absolute;
display: block;
content: "";
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
transform: rotate(-35deg);
}
.brand-logo::before {
width: 20px;
height: 7px;
top: 9px;
left: 7px;
}
.brand-logo i {
width: 15px;
height: 6px;
top: 17px;
left: 11px;
}
.brand-logo::after {
width: 10px;
height: 5px;
top: 25px;
left: 15px;
}
.portal-nav {
justify-content: center;
gap: 8px;
min-width: 0;
height: 100%;
overflow: visible;
}
.portal-nav-group {
position: relative;
display: inline-flex;
align-items: center;
flex: 0 0 auto;
min-height: 72px;
}
.portal-nav-parent,
.portal-nav-toggle,
.portal-nav-child {
display: inline-flex;
align-items: center;
border: 0;
background: transparent;
font: inherit;
cursor: pointer;
}
.portal-nav-parent {
min-height: 36px;
padding: 0 14px;
border-radius: var(--portal-radius-sm);
color: var(--portal-text);
font-size: 15px;
font-weight: 600;
white-space: nowrap;
}
.portal-nav-parent:hover,
.portal-nav-parent.router-link-active,
.portal-nav-group.active .portal-nav-parent {
color: var(--portal-primary-strong);
background: var(--portal-primary-soft);
}
.portal-nav-directory {
cursor: default;
}
.portal-nav-toggle {
justify-content: center;
width: 28px;
height: 28px;
margin-left: -8px;
border-radius: 6px;
color: #64748b;
}
.portal-nav-group:hover .portal-nav-toggle,
.portal-nav-group:focus-within .portal-nav-toggle,
.portal-nav-group.active .portal-nav-toggle {
color: var(--portal-primary-strong);
background: var(--portal-primary-soft);
}
.portal-nav-dropdown {
position: absolute;
top: calc(100% - 8px);
left: 0;
z-index: 30;
display: none;
min-width: 168px;
padding: 8px;
border: 1px solid var(--portal-border);
border-radius: var(--portal-radius-md);
background: var(--portal-surface);
box-shadow: var(--portal-shadow-md);
}
.portal-nav-group:hover .portal-nav-dropdown,
.portal-nav-group:focus-within .portal-nav-dropdown {
display: grid;
gap: 4px;
}
.portal-nav-child {
min-height: 34px;
padding: 0 10px;
border-radius: 6px;
color: #334155;
font-size: 14px;
white-space: nowrap;
}
.portal-nav-child:hover,
.portal-nav-child.router-link-active {
color: var(--portal-primary-strong);
background: var(--portal-primary-soft);
}
.user-area {
justify-content: flex-end;
gap: 10px;
min-width: 130px;
}
.user-chip,
.login-link,
.logout-link {
border: 1px solid var(--portal-border);
color: var(--portal-text-secondary);
background: var(--portal-surface);
cursor: pointer;
}
.user-chip {
gap: 8px;
min-height: 38px;
padding: 0 10px 0 0;
border-radius: 999px;
}
.avatar {
display: inline-grid;
width: 36px;
height: 36px;
place-items: center;
border: 1px solid #bfe4e5;
border-radius: 50%;
color: var(--portal-primary-strong);
background: var(--portal-primary-soft);
font-weight: 700;
}
.logout-link,
.login-link {
min-height: 32px;
padding: 0 10px;
border-radius: var(--portal-radius-sm);
}
.logout-link:hover,
.login-link:hover {
color: var(--portal-primary-strong);
background: var(--portal-primary-soft);
}
.hero-banner {
position: relative;
display: grid;
min-height: 228px;
place-items: center;
overflow: hidden;
background:
linear-gradient(132deg, rgba(20, 124, 131, 0.97), rgba(47, 114, 214, 0.91)),
var(--portal-primary);
}
.hero-pattern {
position: absolute;
inset: 0;
opacity: 0.58;
background:
linear-gradient(42deg, transparent 0 18%, rgba(255, 255, 255, 0.18) 18% 27%, transparent 27% 100%),
linear-gradient(132deg, transparent 0 48%, rgba(246, 186, 51, 0.32) 48% 55%, transparent 55% 100%);
}
.hero-content {
position: relative;
z-index: 1;
width: min(var(--portal-content-width), calc(100% - 48px));
margin: 0 auto;
text-align: left;
}
.hero-eyebrow {
display: inline-block;
margin-bottom: 12px;
color: rgba(255, 255, 255, 0.78);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.18em;
}
.hero-banner h1 {
max-width: 780px;
margin: 0;
color: #ffffff;
font-size: clamp(34px, 4vw, 48px);
font-weight: 700;
letter-spacing: 0;
line-height: 1.2;
text-shadow: 0 12px 28px rgba(15, 23, 42, 0.22);
}
.hero-banner p {
max-width: 620px;
margin: 14px 0 0;
color: rgba(255, 255, 255, 0.86);
font-size: 16px;
}
.portal-main {
width: min(var(--portal-content-width), calc(100% - 48px));
margin: 0 auto;
padding: 32px 0 52px;
}
.portal-main-login {
width: 100%;
min-height: 100vh;
margin: 0;
padding: 0;
}
@media (max-width: 900px) {
.top-nav {
grid-template-columns: 1fr auto;
gap: 10px;
height: auto;
min-height: 56px;
padding: 10px 16px;
}
.portal-nav {
grid-column: 1 / -1;
justify-content: flex-start;
order: 3;
gap: 6px;
overflow-x: auto;
overflow-y: visible;
}
.portal-nav-group {
min-height: 38px;
}
.portal-nav-parent {
min-height: 34px;
padding: 0 12px;
}
.portal-nav-dropdown {
position: fixed;
top: 104px;
left: 16px;
right: 16px;
}
.hero-banner {
min-height: 176px;
}
.hero-content {
width: min(100% - 32px, var(--portal-content-width));
}
.hero-banner p {
margin-top: 10px;
font-size: 14px;
}
.portal-main {
width: min(calc(100% - 28px), var(--portal-content-width));
padding: 22px 0 36px;
}
}
@media (max-width: 560px) {
.brand {
max-width: calc(100vw - 150px);
}
.brand span:last-child {
font-size: 16px;
}
.user-area {
min-width: 0;
}
.user-chip > span:last-child {
display: none;
}
.hero-banner {
min-height: 160px;
}
.hero-eyebrow {
margin-bottom: 8px;
}
}
</style>

View File

@@ -0,0 +1,21 @@
import Vue from "vue"
import ElementUI from "element-ui"
import VueQuillEditor from "vue-quill-editor"
import "element-ui/lib/theme-chalk/index.css"
import "quill/dist/quill.core.css"
import "quill/dist/quill.snow.css"
import App from "./App.vue"
import router from "./router"
import request from "./utils/request"
import dict from "./utils/dict"
Vue.config.productionTip = false
Vue.use(ElementUI)
Vue.use(VueQuillEditor)
Vue.prototype.request = request
Vue.prototype._dict = dict
new Vue({
router,
render: h => h(App)
}).$mount("#app")

View File

@@ -0,0 +1,50 @@
import Vue from "vue"
import Router from "vue-router"
import Login from "@/views/login/index.vue"
import Product from "@/views/demo/product/index.vue"
Vue.use(Router)
const router = new Router({
mode: "hash",
routes: [
{
path: "/",
redirect: "/demo/product"
},
{
path: "/login",
name: "Login",
component: Login
},
{
path: "/demo/product",
name: "Product",
component: Product,
meta: { requiresAuth: false }
},
{
path: "/secondary",
name: "Secondary",
component: Product,
meta: { requiresAuth: false }
}
]
})
router.beforeEach((to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta && record.meta.requiresAuth)
const hasToken = Boolean(localStorage.getItem("portal-token"))
if (requiresAuth && !hasToken) {
next({
path: "/login",
query: { redirect: to.fullPath }
})
return
}
next()
})
export default router

View File

@@ -0,0 +1 @@
body { margin: 0; }

View File

@@ -0,0 +1,32 @@
const dictData = {}
const emptyDict = []
function normalizeOptions(options) {
if (!Array.isArray(options)) {
return emptyDict
}
return options.map((option) => {
const label = option.label || option.name || option.value
const value = option.value || label
return {
...option,
label,
name: option.name || label,
value
}
})
}
export default {
getDictDataByType(type) {
return normalizeOptions(dictData[type])
},
getDictLabel(type, value) {
if (value === undefined || value === null || value === "") {
return "-"
}
const matched = normalizeOptions(dictData[type]).find((option) => String(option.value) === String(value))
return matched ? matched.label : value
}
}

View File

@@ -0,0 +1,6 @@
const page = { data: { records: [], total: 0 } }
export default {
get() { return Promise.resolve(page) },
post(path) { return Promise.resolve(path === '/auth/login' ? { code: '200', data: { token: 'preview-token', user: { displayName: 'Preview User' } } } : { code: '200' }) },
delete() { return Promise.resolve({ code: '200' }) }
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,291 @@
<template>
<section class="portal-login">
<div class="login-visual" aria-hidden="true">
<div class="login-brand">
<span class="login-logo"></span>
<strong>演示商品</strong>
</div>
<h1>欢迎使用</h1>
<p>登录后继续访问项目功能与个人服务</p>
</div>
<div class="login-panel">
<div class="login-heading">
<span>欢迎回来</span>
<h2>账号登录</h2>
</div>
<el-alert
v-if="roleOptions.length === 0"
title="当前没有可登录的前台角色,请联系管理员检查系统角色配置。"
type="warning"
:closable="false"
class="login-alert"
/>
<el-form :model="form" label-position="top" class="login-form" @submit.native.prevent>
<el-form-item label="账号">
<el-input v-model="form.account" placeholder="请输入账号" prefix-icon="el-icon-user" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" placeholder="请输入密码" prefix-icon="el-icon-lock" show-password />
</el-form-item>
<el-button type="primary" class="login-button" :loading="loading" :disabled="roleOptions.length === 0" @click="handleLogin">登录</el-button>
</el-form>
</div>
</section>
</template>
<script>
const roleOptions = [
]
const defaultRole = roleOptions.length > 0 ? roleOptions[0] : null
export default {
name: "PortalLogin",
data() {
return {
loading: false,
roleOptions,
form: {
roleCode: defaultRole ? defaultRole.value : "",
account: defaultRole ? defaultRole.sampleAccount : "",
password: "123456"
}
}
},
watch: {
"form.roleCode"(roleCode) {
const role = this.roleOptions.find(item => item.value === roleCode)
if (role && (!this.form.account || this.roleOptions.some(item => item.sampleAccount === this.form.account))) {
this.form.account = role.sampleAccount
}
}
},
methods: {
handleLogin() {
if (!this.form.roleCode) {
this.$message.warning("请选择登录角色")
return
}
if (!this.form.account || !this.form.password) {
this.$message.warning("请输入账号和密码")
return
}
this.loading = true
this.request.post("/auth/login", this.form)
.then(res => {
if (!res || res.code !== "200" || !res.data || !res.data.token) {
throw new Error((res && res.msg) || "登录失败")
}
localStorage.setItem("portal-token", res.data.token)
localStorage.setItem("portal-user", JSON.stringify(res.data.user || {
account: this.form.account,
displayName: this.form.account,
roleCode: this.form.roleCode
}))
window.dispatchEvent(new Event("portal-user-change"))
this.$message.success("登录成功")
const redirect = this.$route.query.redirect || "/demo/product"
this.$router.push(redirect)
})
.catch(error => {
this.$message.error(error.message || "登录失败")
})
.finally(() => {
this.loading = false
})
}
}
}
</script>
<style scoped>
.portal-login {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(430px, 36vw);
min-height: 100vh;
background:
linear-gradient(132deg, rgba(20, 124, 131, 0.97), rgba(47, 114, 214, 0.91)),
var(--portal-primary, #167d83);
}
.login-visual {
position: relative;
display: flex;
min-width: 0;
flex-direction: column;
justify-content: center;
overflow: hidden;
padding: 72px clamp(48px, 8vw, 128px);
color: #ffffff;
}
.login-visual::before {
position: absolute;
inset: 0;
display: block;
content: "";
opacity: 0.58;
background:
linear-gradient(42deg, transparent 0 18%, rgba(255, 255, 255, 0.18) 18% 27%, transparent 27% 100%),
linear-gradient(132deg, transparent 0 48%, rgba(246, 186, 51, 0.32) 48% 55%, transparent 55% 100%);
}
.login-brand,
.login-visual h1,
.login-visual p {
position: relative;
z-index: 1;
}
.login-brand {
display: inline-flex;
align-items: center;
gap: 12px;
margin-bottom: 40px;
font-size: 22px;
}
.login-logo {
width: 30px;
height: 30px;
border-radius: 9px 19px 9px 19px;
background: linear-gradient(135deg, #6de5e8 0 38%, #f6c85b 38% 66%, #ffffff 66%);
transform: rotate(45deg);
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.18);
}
.login-visual h1,
.login-visual p {
margin: 0;
}
.login-visual h1 {
max-width: 680px;
font-size: clamp(42px, 5vw, 64px);
font-weight: 700;
letter-spacing: 0;
line-height: 1.18;
}
.login-visual p {
max-width: 520px;
margin-top: 20px;
color: rgba(255, 255, 255, 0.88);
font-size: 17px;
line-height: 1.8;
}
.login-panel {
display: flex;
flex-direction: column;
justify-content: center;
padding: clamp(40px, 5vw, 64px);
background: #ffffff;
box-shadow: -18px 0 48px rgba(15, 23, 42, 0.08);
}
.login-heading span {
color: var(--portal-primary, #167d83);
font-size: 14px;
font-weight: 700;
}
.login-heading h2 {
margin: 8px 0 28px;
color: #111827;
font-size: 30px;
font-weight: 700;
letter-spacing: 0;
}
.login-alert {
margin-bottom: 18px;
}
.login-form {
display: grid;
gap: 2px;
}
.role-select,
.login-button {
width: 100%;
}
.login-form >>> .el-input__inner,
.login-form >>> .el-select .el-input__inner {
height: 44px;
border-color: #dfe5ec;
border-radius: 8px;
}
.login-form >>> .el-input__inner:focus {
border-color: var(--portal-primary, #167d83);
box-shadow: 0 0 0 3px rgba(22, 125, 131, 0.12);
}
.login-button {
height: 44px;
margin-top: 10px;
border-color: var(--portal-primary, #167d83);
border-radius: 8px;
background: var(--portal-primary, #167d83);
font-weight: 600;
box-shadow: 0 10px 22px rgba(22, 125, 131, 0.18);
}
.login-button:hover,
.login-button:focus {
border-color: var(--portal-primary-strong, #11656a);
background: var(--portal-primary-strong, #11656a);
}
@media (max-width: 880px) {
.portal-login {
grid-template-columns: 1fr;
}
.login-visual {
min-height: 220px;
padding: 34px 26px;
}
.login-visual h1 {
font-size: 34px;
}
.login-brand {
margin-bottom: 28px;
}
.login-panel {
padding: 34px 24px 44px;
box-shadow: none;
}
}
@media (max-width: 520px) {
.login-visual {
min-height: 184px;
padding: 26px 22px;
}
.login-brand {
margin-bottom: 20px;
font-size: 18px;
}
.login-visual h1 {
font-size: 30px;
}
.login-visual p {
margin-top: 10px;
font-size: 14px;
}
}
</style>

View File

@@ -0,0 +1,18 @@
const port = process.env.PORT || 8081
const apiBaseUrl = process.env.VUE_APP_API_BASE_URL || "http://localhost:8080"
module.exports = {
transpileDependencies: [],
devServer: {
port,
proxy: {
"/api": {
target: apiBaseUrl,
changeOrigin: true,
pathRewrite: {
"^/api": ""
}
}
}
}
}