Repository navigation
feat: chat theme - #7723
feat: chat theme#7723
Conversation
| this.setTheme() | ||
| }, | ||
| }, | ||
| }) |
There was a problem hiding this comment.
Overall, the code is functionally reasonable, but there are a few edge cases and architectural concerns worth addressing.
Potential issues
-
SSR / non-browser safety
document.documentElement.styleis accessed directly inside the store action.- This will throw in SSR, some tests, or non-browser environments.
- Add a guard:
if (typeof document === 'undefined') return
-
DEFAULT_THEME_COLORis assumed valid- User input is validated with
/^#[\da-f]{6}$/, but the default constant is not validated. - If
DEFAULT_THEME_COLORis ever shorthand like#fff, uppercase, or invalid, the RGB parsing can produceNaN, and the default-gradient comparison can behave inconsistently. - Safer approach: normalize/validate the default too, or enforce that it is always a lowercase 6-digit hex string.
- User input is validated with
-
Case-sensitive default comparison
- Input is lowercased, but
DEFAULT_THEME_COLORis compared directly:if (this.themeColor === DEFAULT_THEME_COLOR)
- If
DEFAULT_THEME_COLORis uppercase, e.g.#0052D9, and the user sets#0052d9, the code will treat it as non-default and set gradient variables even though it is the same color. - Compare normalized values:
const defaultColor = DEFAULT_THEME_COLOR.toLowerCase()
- Input is lowercased, but
-
Gradient behavior for custom colors may be intentional but is worth confirming
- For custom colors, both gradient variables are set to the same color:
rootStyle.setProperty('--mk-primary-gradient', this.themeColor) rootStyle.setProperty('--mk-primary-gradient-end', this.themeColor)
- This effectively removes the “blue-purple” gradient.
- If the intent is to keep a gradient, you may want to derive a gradient or only override the primary color. If the intent is to flatten the gradient, this is fine.
- For custom colors, both gradient variables are set to the same color:
-
Initial CSS variables are not applied on store creation
- The store initializes
themeColor, butsetThemeis not called automatically. - If the application relies on these CSS variables and has no CSS fallback, the initial theme may not be applied until
setThemeis called. - Call it during app initialization or ensure CSS defines fallback values.
- The store initializes
-
Store has direct DOM side effects
- This makes the store harder to unit test and couples state management to the DOM.
- Consider extracting DOM application into a helper, composable, plugin, or store subscription. This is not a bug, but it improves separation of concerns.
-
Only supports 6-digit hex colors
#fff,#ffffffff,rgb(...),hsl(...), etc. are rejected.- That may be intentional, but it should be documented as a contract.
Optimization suggestions
- Extract a color-normalization helper.
- Extract the hex-to-RGB conversion.
- Normalize/validate
DEFAULT_THEME_COLOR. - Use a named regex constant.
- Keep the DOM guard.
resetTheme()is fine, but for clarity you could write:this.setTheme(DEFAULT_THEME_COLOR)
Example refactor sketch
const HEX_COLOR_RE = /^#[0-9a-f]{6}$/
const DEFAULT_COLOR = DEFAULT_THEME_COLOR.toLowerCase()
function normalizeColor(color?: string): string {
const normalized = color?.trim().toLowerCase()
return normalized && HEX_COLOR_RE.test(normalized) ? normalized : DEFAULT_COLOR
}
function hexToRgbChannels(hex: string): string {
const value = hex.slice(1)
const r = Number.parseInt(value.slice(0, 2), 16)
const g = Number.parseInt(value.slice(2, 4), 16)
const b = Number.parseInt(value.slice(4, 6), 16)
return `${r} ${g} ${b}`
}
export const useChatThemeStore = defineStore('chat-theme', {
state: () => ({ themeColor: DEFAULT_COLOR }),
actions: {
setTheme(themeColor?: string) {
const color = normalizeColor(themeColor)
this.themeColor = color
if (typeof document === 'undefined') return
const rootStyle = document.documentElement.style
rootStyle.setProperty('--mk-primary', color)
rootStyle.setProperty('--mk-primary-rgb', hexToRgbChannels(color))
if (color === DEFAULT_COLOR) {
rootStyle.removeProperty('--mk-primary-gradient')
rootStyle.removeProperty('--mk-primary-gradient-end')
} else {
rootStyle.setProperty('--mk-primary-gradient', color)
rootStyle.setProperty('--mk-primary-gradient-end', color)
}
},
resetTheme() {
this.setTheme(DEFAULT_COLOR)
},
},
})No major functional bug is present if DEFAULT_THEME_COLOR is guaranteed to be a lowercase #rrggbb value and the store is only used client-side. The main improvements are SSR safety, default-color normalization, and separating DOM side effects from store state management.
| }) | ||
| }, | ||
|
|
||
| /** 切换当前场景,由守卫在放行前调用。 */ |
There was a problem hiding this comment.
Main concern: fetchAuthProfile now mixes data fetching with a theme side effect. That makes the action less predictable and can create race conditions.
Potential issues
-
Side effect in a fetch action
fetchAuthProfileis named/typed like a pure fetch, but now mutates global theme state.- Any caller that only needs the profile will also accidentally change the theme.
- Prefer applying the theme in the guard after the scope is confirmed, or add an explicit option such as
applyTheme.
-
Race condition / out-of-order responses
- If
fetchAuthProfile('A')is started, thenfetchAuthProfile('B')starts and resolves first,Acan resolve later and overwrite the theme with A’s theme. - Guard the side effect, e.g. only apply when the requested scope is still the current scope:
if (scope === this.scope) { useChatThemeStore().setTheme(authProfile.custom_theme?.theme_color) }
- If
this.scopeis updated after this call, move the theme application to the guard aftersetScope.
- If
-
Repeated cached calls still reapply the theme
- Even when
scope === this.scope && this.authProfilereuses the cached profile, the theme is applied again. - If the user can manually change the theme, later calls to
fetchAuthProfilemay unexpectedly reset it. - Consider applying the theme only when the profile is first loaded or when the scope changes.
- Even when
-
Undefined theme value
authProfile.custom_theme?.theme_colormay beundefined.- Check
setThemebehavior:- If it ignores
undefined, stale themes from the previous scope may remain. - If it resets to default, make that behavior explicit.
- If it ignores
- Safer:
or explicitly apply a default/reset value if that is intended.
const themeColor = authProfile.custom_theme?.theme_color if (themeColor) { useChatThemeStore().setTheme(themeColor) }
-
Possible circular dependency
authnow importstheme. Verify thatthemedoes not importauth, otherwise circular imports can cause initialization issues.
-
No in-flight request deduplication
- Concurrent calls for the same scope before
authProfileis cached will trigger multiple API requests. - Optional optimization: cache the pending promise per scope.
- Concurrent calls for the same scope before
-
Readability
- The nested ternary is harder to read.
async/awaitwould be clearer:async fetchAuthProfile(scope: string): Promise<AuthProfile<unknown>> { const authProfile = scope === this.scope && this.authProfile ? this.authProfile : isPortalScope(scope) ? await ChatAuthApi.getPortalAuthProfile() : await ChatAuthApi.getApplicationAuthProfile(scope) if (scope === this.scope) { useChatThemeStore().setTheme(authProfile.custom_theme?.theme_color) } return authProfile }
- The nested ternary is harder to read.
Recommendation
Keep fetchAuthProfile focused on fetching/caching, and apply the theme in the caller/guard where the current scope is known. If the side effect must stay here, guard it by current scope, handle undefined theme explicitly, and avoid reapplying it on cached calls unless that is intentional.
| <el-button :type="is_submit ? 'info' : 'primary'" :disabled="is_submit || READONLY_VIEWS.includes(viewType) || !sendMessage" @click="submit"> | ||
| {{ is_submit ? '已提交' : '提交' }} | ||
| </el-button> | ||
| </div> |
There was a problem hiding this comment.
Overall, the diff is mostly formatting/import-style cleanup and does not change the main logic. However, there are a few points worth checking:
Potential issues
-
border-N300was removed- <div class="mk-gray-card-lg space-y-4 border border-N300"> + <div class="mk-gray-card-lg space-y-4 border">
- If
border-N300was a valid design token, this is a visual regression: the border color will change to the default/inherited color. - If
border-N300was invalid or no longer used, this is fine, but prefer using a valid token such asborder-neutral-300or the project’s actual token. - Recommendation: Restore
border-N300unless the color change is intentional.
- If
-
Long template lines may conflict with the formatter/linter
<MkDynamicsForm ref="dynamicsFormRef" :view="..." :render-data="..." v-model="form_data" /> <el-button :type="..." :disabled="..." @click="submit">
- The original code was wrapped across multiple lines; this diff makes the lines longer.
- If the project uses Prettier or
vue/max-attributes-per-line, this may fail formatting/lint checks. - Recommendation: Run the project formatter and keep the formatted output.
-
Duplicate readonly condition
:view="is_submit || READONLY_VIEWS.includes(viewType)" :disabled="is_submit || READONLY_VIEWS.includes(viewType) || !sendMessage"
- The same logic is repeated. Extracting it into a computed property reduces duplication and keeps the form and button in sync.
- Assuming
is_submitandviewTypeare refs:
const isReadonly = computed( () => is_submit.value || READONLY_VIEWS.includes(viewType.value) )
Then:
<MkDynamicsForm :view="isReadonly" ... /> <el-button :disabled="isReadonly || !sendMessage" ...>
- This is a readability/maintainability improvement, not a functional bug.
-
Duplicate
editorIdon twoMdPreviewcomponents<MdPreview editorId="conversation-text" ... /> ... <MdPreview editorId="conversation-text" ... />
- If
editorIdis used as a DOM id or editor instance key, using it twice may cause conflicts. - Recommendation: Use unique IDs, for example:
editorId="conversation-leading-text" editorId="conversation-trailing-text"
- This issue already existed, but it is worth fixing.
- If
-
submitshould also guard against readonly/duplicate submission- The button is disabled, but defensive code inside
submitis still useful:
if (isReadonly.value || !sendMessage) return
- If
submitperforms an async request, ensure it cannot be triggered twice while pending. Set a submitting flag beforeawaitand reset it infinally.
- The button is disabled, but defensive code inside
Minor suggestions
- The import quote change from
"to'is fine and likely consistent with project style. - Removing the blank line before
</script>is harmless. - Snake_case names such as
is_submit,form_data, andform_field_listare inconsistent with typical TS/Vue camelCase style, but this is only a style issue if the project enforces it. READONLY_VIEWS.includes(...)is fine for two items. If the list grows significantly, consider aSet<ViewType>:const READONLY_VIEWS = new Set<ViewType>([VIEW_TYPE.LOG, VIEW_TYPE.SHARE]) // READONLY_VIEWS.has(viewType.value)
Most important action
Check whether removing border-N300 is intentional. If not, restore it. Then run the project formatter and consider extracting the repeated readonly condition into a computed property.
feat: chat theme